一、背景
在开发中,对于一个业务维度的数据,需要每次获取当前业务的子业务下的最大id值,并在这个最大值上自增1以后作为新的子业务id,例如:
主业务:
id | filed |
---|---|
1 | … |
子业务:
id | parentId | filed |
---|---|---|
1 | 1 | … |
2 | 1 | … |
1 | 2 | … |
2 | 2 | … |
我们要插入一条数据的时候,子业务的id要自增(不能单纯的使用数据库的自增),我们能想到的办法就是使用sql查出最大值(select max(id) from 子业务 where parendId = 1
),并+1作为新的子项目id,但是在分布式环境下就有问题,对同一个父项目的子项目而言有可能会出现多个相同的子项目id。
二、解决方案
我们可以使用Redis 缓存每个父项目对应子业务的最大id,每次获取的时候就返回最大值+1,并把这个值设置为新的缓存值
2.1 定义注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisIncrementId {
String name();
String[] params();
int expireTime() default 3600*24;
String tenantId();
}
2.2 定义切面
@Aspect
@Component
public class RedisAOP {
@Around("@annotation(RedisIncrementId)")
public Object redisIncrementId(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 获取租户信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//类路径名
String classPathName = joinPoint.getTarget().getClass().getName();
//类名
String className = classPathName.substring(classPathName.lastIndexOf(".") + 1, classPathName.length());
//获取方法名
String methodName = signature.getMethod().getName();
String[] parameterNames = signature.getParameterNames();
// 获取切面注解参数
RedisIncrementId annotation = signature.getMethod().getAnnotation(RedisIncrementId.class);
String name = annotation.name();
if(name.isEmpty()){
name = className + "." + methodName;
}
if (StringUtils.isBlank(name)) {
return joinPoint.proceed();
}
String[] params = annotation.params();
int expireTime = annotation.expireTime();
String tenantIdFiled = annotation.tenantId();
String tenantId = getFiledValue(args, parameterNames, tenantIdFiled).toString();
String sName = commonConfig.getRedis().getAppName() + "_";
sName += (tenantId.isEmpty() ? authUserHelper.getCurrentTenantId() : tenantId) + "_";
String rKey = sName + name;
StringBuilder argStr = new StringBuilder();
for (String p : params) {
argStr.append(getFiledValue(args, parameterNames, p));
}
if (StringUtils.isNotBlank(argStr.toString())) {
rKey += "_" + argStr;
}
Object ret = null;
try (val jedis = redisConfig.jedisPool.getResource()) {
// 获取value,并且在获取后对原值+1重新放在redis中
// Lua脚本
String luaScript =
"local current_value = redis.call('GET', KEYS[1]) " +
"if current_value then " +
" local new_value = tonumber(current_value) + 1 " +
" redis.call('SET', KEYS[1], new_value) " +
" redis.call('EXPIRE', KEYS[1], ARGV[1]) " +
" return new_value " +
"else " +
" return 0 " +
"end";
// 执行Lua脚本
long newValue = 0;
Object eval = jedis.eval(luaScript, 1, rKey, String.valueOf(expireTime));
if (eval instanceof Long) {
newValue = (Long) eval;
} else if (eval instanceof String) {
newValue = Long.parseLong((String) eval);
} else {
newValue = Long.parseLong(eval.toString());
}
if (newValue == 0) {
// key不存在
ret = joinPoint.proceed();
if (ret instanceof Long || ret instanceof Integer) {
Long setnx = jedis.setnx(rKey, ret.toString());
// 没设置成功说明别其他线程设置了,直接重新获取一下
if (setnx == 0) {
ret = jedis.eval(luaScript, 1, rKey, String.valueOf(expireTime));
}
jedis.expire(rKey, expireTime);
}
} else {
// key 存在
ret = newValue;
jedis.expire(rKey, expireTime);
}
return ret;
} catch (Exception ex) {
logger.error("redis cache error:", ex);
if (ret != null) {
return ret;
}
return joinPoint.proceed();
}
}
private Object getFiledValue(Object[] args, String[] parameterNames, String filed) {
String[] split = filed.split("\\.");
String objectName = "";
String filedName = "";
if (split.length > 1) {
objectName = split[0];
filedName = split[1];
int keyIndex = ArrayUtils.indexOf(parameterNames, objectName);
Object object = args[keyIndex];
return ReflectUtil.getFieldValue(object, filedName);
} else {
filedName = split[0];
int keyIndex = ArrayUtils.indexOf(parameterNames, filedName);
return args[keyIndex];
}
}
}
2.3 使用
@RedisIncrementId(name = "getId", params = {"dto.param1", "dto.param2", "dto.param3", "dto.param4"}, tenantId = "dto.param5")
@Override
public Long getId(Dto dto) {
// 从数据库获取子项目最大值
return l + 1;
}
}
三、改进之处
如果是需要批量获取多个id,那么自增的步长就要能自定义,而不是一次增加1