由于项目开发中设计到游戏运营平台的搭建,游戏中每一个不同的区服使用的是不同的数据库存储数据。 如:
区服1:包括 game_1_data(游戏数据数据库),game_1_log(游戏日志数据库);
区服2:包括 game_2_data(游戏数据数据库),game_2_log(游戏日志数据库);
… 并且之后会持续增多
除了以上的数据库还包含一些,游戏全局的库等,以及平台本身的数据库;那么在单体项目中如何切换每一个请求应该查询那个数据库,成为一个难点。
/**
* 查询指定区服的聊天记录 game_{0}_log 数据库名称格式,
* ChatMonitorSearchDTO.xyServerId 请求参数中指定的区服ID
* 若ChatMonitorSearchDTO.xyServerId=1, 则查询数据库game_1_log
*/
@SelectDB(dbName = "game_{0}_log", serverFiled = "ChatMonitorSearchDTO.xyServerId")
public Future<IPage<ChatMonitorListVO>> pageList(ChatMonitorSearchDTO DTO) throws Exception{
IPage<ChatMonitorListVO> page = new Page<>(DTO.getCurrent(), DTO.getSize());
page = chatMonitorMapper.pageList(page, DTO);
return new AsyncResult(page);
}
Map中。AbstractRoutingDataSource+ThreadLocal+AOP配合使用,确保可以修改每条线程的数据源。@SelectDB会新开线程执行。/**
* 初始化数据源
*/
@Component
public class JavaCodeDataSourceProvider implements ApplicationListener<ContextRefreshedEvent> {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Autowired
private DataSourceInfoServiceImpl dataSourceInfoService;
// 初始化本平台的数据源
@PostConstruct
public void init() {
DynamicDataSourceService.addDataSource(DynamicDataSource.DEFAULT_DB_KEY, url, username, password);
}
// 查询本平台数据库中,配置的游戏数据库的连接信息,并加载到 Map
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
dataSourceInfoService.buildDynamicDataSourceFromDB();
}
}
@Service
public class DataSourceInfoServiceImpl {
@Autowired
private DataSourceInfoMapper dataSourceInfoMapper;
/**
* 从数据库中查询配置的数据库链接信息,构建动态的数据源
*/
public void buildDynamicDataSourceFromDB(){
List<DataSourceInfo> dataSourceInfos = dataSourceInfoMapper.selectList(Wrappers.lambdaQuery(DataSourceInfo.class));
for (DataSourceInfo info : dataSourceInfos){
DynamicDataSourceService.addDataSource(info.getDbName(), info.getUrl(), info.getUsername(), info.getPassword());
}
}
}
/**
* 维护动态数据源
*/
@Slf4j
public class DynamicDataSourceService {
private static final Map<Object, Object> dataSources = new HashMap<>();
private static final ThreadLocal<String> dbKeys = ThreadLocal.withInitial(() -> null);
/**
* 构建DataSource
* @param url 数据库地址
* @param username 用户名
* @param password 用户密码
* @return DataSource
*/
public static DataSource buildDataSource(String url, String username, String password) {
DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName("com.mysql.cj.jdbc.Driver");
builder.username(username);
builder.password(password);
builder.url(url);
return builder.build();
}
/**
* 动态添加一个数据源
* @param name 数据源的key
* @param dataSource 数据源对象
*/
public static void addDataSource(String name, DataSource dataSource) {
DynamicDataSource dynamicDataSource = SpringUtils.getBean(DynamicDataSource.class);
dataSources.put(name, dataSource);
dynamicDataSource.setTargetDataSources(dataSources);
dynamicDataSource.afterPropertiesSet();
log.info("添加了数据源:{}", name);
}
/**
* 动态添加一个数据源
* @param dbName 数据源的key
* @param url 数据库地址
* @param username 用户名
* @param password 用户密码
*/
public static void addDataSource(String dbName, String url, String username, String password){
DataSource dataSource = buildDataSource(url, username, password);
addDataSource(dbName, dataSource);
}
/**
* 是否存在数据源
*/
public static boolean exist(String dbKey) {
return dataSources.get(dbKey) != null;
}
/**
* 切换数据源
*/
public static void switchDb(String dbKey) {
dbKeys.set(dbKey);
}
/**
* 重置数据源
*/
public static void resetDb() {
dbKeys.remove();
}
/**
* 获取当前数据源
*/
public static String currentDb() {
return dbKeys.get();
}
}
@Configuration
public class DynamicDataSourceConfig {
/**
* 动态数据源
*/
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
sqlSessionFactoryBean.setTypeAliasesPackage("com.qykj.xyj.**.entity");
sqlSessionFactoryBean.setTypeEnumsPackage("com.qykj.xiyouji.**.enums");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*/*Mapper*.xml"));
GlobalConfig globalConfig = new GlobalConfig();
// 配置自定义填充器 MyMetaObjectHandler
globalConfig.setMetaObjectHandler(new MybatisPlusMetaObjectHandler() );
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
// 逻辑删除配置
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteField("del");
dbConfig.setLogicDeleteValue("1");
dbConfig.setLogicNotDeleteValue("0");
globalConfig.setDbConfig(dbConfig);
// 设置XML
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
configuration.setJdbcTypeForNull(JdbcType.NULL);
// 设置sql日志
configuration.setLogImpl(StdOutImpl.class);
// 设置枚举处理器
configuration.setDefaultEnumTypeHandler(EnumValueTypeHandler.class);
sqlSessionFactoryBean.setConfiguration(configuration);
// 配置分页插件
sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());
// 配置事务管理器
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
/**
* 配置分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setDialect(new MySqlDialect());
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
public static final String DEFAULT_DB_KEY = "game_base";
@Override
protected Object determineCurrentLookupKey() {
String currentDb = DynamicDataSourceService.currentDb();
log.info("currentDb:"+currentDb);
if (currentDb == null) {
return DEFAULT_DB_KEY;
}
return currentDb;
}
}
@SelectDB的编写@Async(ThreadPoolConfig.THREAD_POOL),确保切换数据源之后为另一个线程。
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Async(ThreadPoolConfig.THREAD_POOL)
public @interface SelectDB {
// 数据库名称
String dbName();
// 查询参数中标识区服的字段
String serverFiled() default "-1";
}
@Order(3)
@Slf4j
@Aspect
@Component
public class SelectDBAspect {
@Value("${gjxy.db.url}")
private String url;
@Value("${gjxy.db.username}")
private String username;
@Value("${gjxy.db.password}")
private String password;
@Pointcut("@annotation(com.xxx.aspect.SelectDB)")
public void pointcut(){}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取接口上 SelectDB 注解
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = methodSignature.getMethod();
SelectDB annotation = method.getAnnotation(SelectDB.class);
String dbName = annotation.dbName();
String serverFiled = annotation.serverFiled();
if("-1".equals(serverFiled)){
// 切换到对应的库,不区分区服的库
DynamicDataSourceService.switchDb(dbName);
} else {
// 切换到对应的库,根据区服切换到不同的库
Object[] strArr = AnalyzeParamsUtils.analyzeParams(point, serverFiled);
dbName = MessageFormat.format(dbName, strArr);
boolean exist = DynamicDataSourceService.exist(dbName);
if(exist){
DynamicDataSourceService.switchDb(dbName);
}else {
// 确保开启新的区服后会新增game_x_data,game_x_log两个库,也能够正常查询
String urlFormat = MessageFormat.format(url, dbName);
DynamicDataSourceService.addDataSource(dbName, urlFormat, username, password);
DynamicDataSourceService.switchDb(dbName);
}
}
Object proceed = point.proceed();
DynamicDataSourceService.resetDb();
return proceed;
}
}
AbstractRoutingDataSource+ThreadLocal+AOP配合使用,确保可以修改每条线程的数据源。Map中的key,value就是指定的数据源。