关于读写分离强制走主库的实现逻辑
背景:之前做过读写分离的,肯定都会遇到这么一种情况,写完立刻读,有可能导致因为数据还没有同步到从库,导致造成脏读问题。刚开始解决方案是对这些接口都指定强制读主库。但是因为系统大多数都是读多写少,接口总是强制读主库,从库就失去了它的意义,所以为此改进了强制读主库的逻辑,实现尽可能利用读写分离的优点。优化思路:例如当修改订单状态后,把对应单据号指定500毫秒过期时间存入redis中,下次查询该单据信息先看500毫秒是否被修改过,是就读主库否就读从库
下面是优化方案:
定义注解,实现统一切面:
package com.zhqc.cloud.wms.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 主库路由注解
*
* @author zdd
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterRouteOnly {
String value() default "";
/**
* 前缀
*/
String prefix() default "";
}
- 定义切面
package com.zhqc.cloud.wms.aop;
import cn.hutool.core.util.ReflectUtil;
import com.zhqc.cloud.wms.annotation.MasterRouteOnly;
import groovy.util.logging.Slf4j;
import io.shardingsphere.api.HintManager;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author zdd
*/
@Aspect
@Component
@Slf4j
public class MasterRouteAspect {
@Resource
private MasterRouteRedis masterRouteRedis;
private static final Logger log = LoggerFactory.getLogger(MasterRouteAspect.class);
@Around("@annotation(masterRouteOnly)")
public Object setMasterRoute(ProceedingJoinPoint joinPoint, MasterRouteOnly masterRouteOnly) throws Throwable {
try {
//获取注解的value数据
String value = masterRouteOnly.value();
String prefix = masterRouteOnly.prefix();
//判断value是否是EL表达式
if (StringUtils.isNotBlank(value) && StringUtils.isNotBlank(prefix)) {
if(isEl(value)) {
value = parseEl(value, joinPoint).toString();
}
Object obj = masterRouteRedis.get(prefix + value);
if (obj != null) {
HintManager.getInstance().setMasterRouteOnly();
}
}else {
HintManager.getInstance().setMasterRouteOnly();
}
} catch (Exception e) {
log.error("主库路由注解解析错误: {}", ((MethodSignature) joinPoint.getSignature()).toString());
}
return joinPoint.proceed();
}
private boolean isEl(String value) {
return value.startsWith("#{") && value.endsWith("}");
}
private Object parseEl(String value, ProceedingJoinPoint joinPoint) {
//去掉头尾
String el = value.substring(2, value.length() - 1);
//按照.分割
String[] split = el.split("\\.");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
int index = -1;
for (int i = 0; i < parameterNames.length; i++) {
if (split[0].equals(parameterNames[i])) {
index = i;
break;
}
}
if (index == -1) {
log.error("注解EL表达式定义错误: {}", ((MethodSignature) joinPoint.getSignature()).toString());
}
Object arg = joinPoint.getArgs()[index];
if(split.length == 1){
return arg;
}else {
for (int i = 1; i < split.length; i++) {
arg = ReflectUtil.getFieldValue(arg, split[i]);
}
}
return arg;
}
}
- redis命令封装类
package com.zhqc.cloud.wms.aop;
import com.zhqc.cloud.common.constants.CommonConstants;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author zdd
*/
@Component
public class MasterRouteRedis {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
return redisTemplate.opsForValue().get(CommonConstants.READ_MASTER_FLAG + key);
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(CommonConstants.READ_MASTER_FLAG + key, CommonConstants.READ_MASTER_FLAG + value,
CommonConstants.READ_MASTER_FLAG_EXPIRE, TimeUnit.MILLISECONDS);
}
}
- CommonConstants
package com.zhqc.cloud.common.constants;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
public class CommonConstants {
/**
* 读主库标识前缀(redis)
*/
public static final String READ_MASTER_FLAG = "readMasterFlag:";
/**
* 读主库标识存活时间ms(redis)
*/
public static final long READ_MASTER_FLAG_EXPIRE = 500;
/**
* pageInfo 接口
*/
public static final String PAGE_INFO = "pageInfo";
}
-
使用
有两种使用方式,一种直接加注解即可,标识这个方法里查询总会查主库;另外一种就是指定el表达式以及前缀,提取并组装key,从redis读取key,有就读主没有就读从。
总结:个人的一个小优化,如果有更好的解决方案,可以进行评论,互相学习。