Spring整合Sharding实现查询走指定数据库

一、项目背景

  • 数据库采用MySQL一主多从,使用Sharding实现主从读写分离。由于大数据报表,MySQL查询很慢,所以报表的查询走列式数据库。现在需要指定某些查询走列数据库,某些走主库,其他查询实现MySQL从库负载均衡。

二、实现思路

  1. Spring整合Sharding实现主从读写分离。
  2. 在Mapper上添加自定义注解,注解用于标识哪些方法走哪些库
  3. 自定义Mybatis拦截器拦截所有查询请求,获取请求的Mapper以及具体方法,获取Mapper上的注解根据方法名称找指定的数据库,将数据库标识设置到线程副本中
  4. 自定义Sharding负载均衡策略,根据线程副本获取指定数据库标识,将该数据库返回,从而实现查询走指定数据库
  5. 配合nacos配置中心,实现动态开启关闭使用列数据库查询。

三、Spring整合Sharding,配置主从数据库

  • 网上一搜一大把,这里不再赘述

四、自定义注解

  • MasterAnotation 标注的方法,表示查询走主库
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MasterAnotation {
	
	String[] methods() ;

}

  • ColumnarAnotation 标注的方法,表示查询列数据库
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ColumnarAnotation {
	
	String[] methods() ;

}

五、添加MyBatis拦截器,拦截所有查询

  • ShardingInterceptor 用于拦截所有查询请求
  • 拦截器中注入的ShardingUtil对象,用于获取具体Mapper以及方法,根据注解将数据库标识设置到线程副本中,查询结束清空线程副本。

@Intercepts({
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class }),
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }),
 })
public class ShardingInterceptor implements Interceptor {
	
	private ShardingUtil shardingUtil;
	
	public ShardingInterceptor(ShardingUtil shardingUtil){
		this.shardingUtil=shardingUtil;
	}
	
	
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Object[] args = invocation.getArgs();
		MappedStatement ms=(MappedStatement) args[0];
		//查询之前做一些处理
		shardingUtil.beforeStarRocksProcess(ms);
		Object obj = invocation.proceed();
		//查询之后做一些处理
		shardingUtil.afterStarRocksProcess();
		return obj;
	}
	
	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		}
		return target;
	}

	@Override
	public void setProperties(Properties properties) {
		// TODO Auto-generated method stub

	}

}

六、创建线程副本管理器

public class ShardingManager {
	
	
	/**
	 * 存储当前线程的数据库信息
	 */
	private static final ThreadLocal<String> SLAVE_INFO = new ThreadLocal<>();
	
	public static String getSlave(){
		return SLAVE_INFO.get();
	}
	
	public static void setSlave(String slaveName){
		SLAVE_INFO.set(slaveName);
	}
	
	public static void clear(){
		SLAVE_INFO.remove();
	}

}

七、自定义负载均衡策略

  • 自定义负载均衡策略,根据线程副本获取当前线程的数据库标识,查询则返回该数据库
@Getter
@Setter
public  class ShardingMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {

	private static final ConcurrentHashMap<String, AtomicInteger> COUNTS = new ConcurrentHashMap<>();

	private Properties properties = new Properties();

	@Override
	public String getType() {
		return "CUSTOM";
	}

	@Override
	public String getDataSource(final String name, final String masterDataSourceName,final List<String> slaveDataSourceNames) {
		//获取线程副本
		String slaveName=Optional.ofNullable(ShardingManager.getSlave()).orElse(DBConstant.SLAVE);
		//判断是否走主数据库
		if(slaveName.equals(masterDataSourceName)){
			return masterDataSourceName;
		}
		//判断是否走列数据库
		if(slaveName.equals(DBConstant.COLUMNAR)){
			return DBConstant.COLUMNAR;
		}
		//获取从库
		List<String> realSlaveDataSourceNames=new ArrayList<String>();
		slaveDataSourceNames.forEach(item->{
			if(!item.equals(DBConstant.COLUMNAR)){
				realSlaveDataSourceNames.add(item);
			}
		});
		//不存在从库,直接走主库
		if(ObjectUtils.isEmpty(realSlaveDataSourceNames)){
			return masterDataSourceName;
		}
		//随机获取从库
		AtomicInteger count = COUNTS.containsKey(name) ? COUNTS.get(name) : new AtomicInteger(0);
		COUNTS.putIfAbsent(name, count);
		count.compareAndSet(realSlaveDataSourceNames.size(), 0);
		return realSlaveDataSourceNames.get(Math.abs(count.getAndIncrement()) % realSlaveDataSourceNames.size());
	}

}

八、处理拦截的查询

  • ShardingUtil 用于处理拦截的查询,获取Mapper方法指定的数据库,将数据库标识存入线程副本。
  • 同时引入enableColumnar属性用于动态启动关闭列数据库,当列数据库出现问题或者列数据库需重新全量同步MySQL数据时,通过该属性动态关闭使用列数据库。
@Slf4j
@Component
public class ShardingUtil {
	
	/**
 	 * 动态刷新配置,可在nacos中动态切换,列数据库出现问题,切换该参数为false直接走MySQL数据库
 	 */
 	@Value("${enable.columnar:true}")
    private boolean enableColumnar;

	
	public  void beforeStarRocksProcess(MappedStatement mappedStatement) throws Exception {
		//清空线程副本,防止遗留副本产生影响
		ShardingManager.clear();
		//走主库
		if(isMaster(mappedStatement)){
			//不使用HinManager强制主库路由,在事务中,强制路由后,后续查询都走主库
			ShardingManager.setSlave(DBConstant.MASTER);
			return ;
		}
		//走MySQL数据库
		if(!enableStarRocks){
			ShardingManager.setSlave(DBConstant.SLAVE);
			return ;
		}
		//走列数据库
		if(isColumnar(mappedStatement)){
			ShardingManager.setSlave(DBConstant.COLUMNAR);
		}
	
	}


	public  void afterStarRocksProcess() throws Exception {
		ShardingManager.clear();
	}
	

	private  boolean isColumnar(MappedStatement mappedStatement)throws Exception{
		return isAnnotation(mappedStatement, classType->classType.isAnnotationPresent(StarRocksAnotation.class),
				classType->classType.getAnnotation(StarRocksAnotation.class).methods());
	}
	

	private  boolean isMaster(MappedStatement mappedStatement)throws Exception{
		return isAnnotation(mappedStatement, classType->classType.isAnnotationPresent(MasterAnotation.class),
				classType-> classType.getAnnotation(MasterAnotation.class).methods());
	}
	
	
	private boolean isAnnotation(MappedStatement mappedStatement,Predicate<Class<?>> isAnnotationPresent,Function<Class<?>, String[]> methodFunction) throws Exception{
		String id = mappedStatement.getId();
		String classMapper = id.substring(0, id.lastIndexOf("."));
		Class<?> classType = Class.forName(classMapper);
		if (isAnnotationPresent.test(classType)) {
			//获取mapper的方法
			String method = id.replace(String.format("%s.", classMapper), "");
			String[] methodArr=methodFunction.apply(classType);
			List<String> methods = new ArrayList<>(Arrays.asList(methodArr)); 
			methods.add("selectList_COUNT");
			return methods.contains(method);
		}
		return false;
	}
	
	public void setColumnarStatus(String value){
		if(!ObjectUtils.isEmpty(value)){
			this.enableStarRocks=value.equals("false")?false:true;
			if(enableStarRocks){
				log.info("=====>启用Columnar");
			}else{
				log.info("=====>禁用Columnar");
			}
		}
	}
	
}

九、注入拦截器

  • 将ShardingInterceptor拦截器通过Spring SPI 等技术注入,同时将ShardingUtil 注入到该拦截器中。方法多样,这里就不再赘述。

至此撒花结束!!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 引入依赖 在pom.xml添加以下依赖: ```xml <!-- ShardingSphere-JDBC Spring-Boot-Starter --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>5.0.0-alpha</version> </dependency> <!-- MySQL Connector --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.20</version> </dependency> ``` 2. 数据库配置 在application.yml中添加数据库配置: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver shardingsphere: datasource: names: ds0 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useSSL=false username: root password: 123456 sharding: tables: order_info: actual-data-nodes: ds0.order_info_$->{0..1} table-strategy: inline: sharding-column: order_id algorithm-expression: order_info_$->{order_id % 2} key-generator: type: SNOWFLAKE column: order_id ``` 3. 表分片策略 上面的配置中,我们配置了对order_info表进行分片,分成两个表order_info_0和order_info_1,根据order_id字段对数据进行分片。我们使用了SNOWFLAKE算法对order_id生成分布式ID。实际的数据表名为order_info_0和order_info_1。 4. 在代码中使用 在Service层代码中操作order_info表时,可以使用ShardingJdbcTemplate进行操作。首先注入ShardingJdbcTemplate: ```java @Autowired private ShardingJdbcTemplate shardingJdbcTemplate; ``` 接下来就可以像操作普通的JdbcTemplate一样使用ShardingJdbcTemplate进行操作了: ```java String insertSql = "insert into order_info (order_id, user_id, order_time) values (?, ?, ?)"; shardingJdbcTemplate.update(insertSql, 1, 1, new Date()); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值