基于Redis的一种自增的分布式id实现

一、背景

在开发中,对于一个业务维度的数据,需要每次获取当前业务的子业务下的最大id值,并在这个最大值上自增1以后作为新的子业务id,例如:
主业务:

idfiled
1

子业务:

idparentIdfiled
11
21
12
22

我们要插入一条数据的时候,子业务的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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值