高并发情况下使用Redis采用Lua脚本生成多个唯一的流水号
业务场景:
业务需求需要在高并发的情况下生成唯一的流水号,并且返回相应并发数量的流水号
模块+年+月作为Key值,如果在redis中不存在该模块的key则从数据库查询最新的模块流水号并加1设置到redis,且返回
主要问题:
1、需要保证高并发情况下流水号的唯一性
2、需要保证多个并发请求都可以拿到唯一的流水号
方案一:采用synchronized锁的方式
*缺陷:无法解决多节点服务问题*
使用本地锁的方式锁住生成流水号的业务代码
方案二:watch+redis 事务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hEb3ZIlS-1634627454140)(file:///C:\Users\zzf\AppData\Local\Temp\ksohtml\wps2739.tmp.jpg)]
*缺陷:只能确保在并发情况下流水号的唯一,而无法生成对应并发数量的流水号*
*原因:watch 监听key 的模式是采用CAS乐观锁的方式,所以监听不到其它修改了key 并且key值未发生改变的情况,所以监听不会触发,会导致事务多个都可以成功,无法采用事务失败重试生成流水号*
相关链接:
RedisTemplate 事务处理方法 watch multi exec 的使用
springboot2.0版本之前使用redis事务关闭问题
方案三:使用分布式锁
1、使用分布式锁 redis nx锁
2、使用redisson
方案四:使用消息队列
利用消息中间件进行处理,将并发改变成串行化生成
方案五:采用redis lua脚本
实现代码:
// 生成流水号方法
private BigDecimal getSerialNumber(String yearMouth, String serialNumberPrefix, BalanceModule balanceModule) {
String key = RedisConstant.MODULE_YEAR_MONTH_SERIAL_NUMBER + serialNumberPrefix + ":" + yearMouth;
// 数据库查询模块流水号并且自动版本加1,最新可使用的流水号
BigDecimal serialNumberStartOnBatch = getSerialNumberStartOnBatch(balanceModule, Integer.valueOf(yearMouth));
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_serial_number.lua")));
Long execute = redisTemplate.execute(redisScript, Collections.singletonList(key), serialNumberStartOnBatch + "");
return BigDecimal.valueOf(execute);
}
// 数据库查询模块流水号
private BigDecimal getSerialNumberStartOnBatch(BalanceModule module, Integer yearMonth) {
String prefix = module.getSerialNumberPrefix() + "-" + yearMonth;
// 获取主表中对应年月最新的流水号
String latestSerialNumberInBalance = frsBalanceService.getLatestSerialNumberInBalance(module.name(), prefix);
// 获取日志表中对应年月最新的流水号
String latestSerialNumberInLog = frsBalanceService.getLatestSerialNumberInLogBy(module.getModuleName(), BalanceOperateType.TEMPLATE.getType(), prefix);
if (latestSerialNumberInBalance == null && latestSerialNumberInLog == null) {
return BigDecimal.ONE;
}
BigDecimal balanceSerialNumber = SerialNumberUtils.getNumber(latestSerialNumberInBalance);
BigDecimal logSerialNumber = SerialNumberUtils.getNumber(latestSerialNumberInLog);
BigDecimal result = balanceSerialNumber.compareTo(logSerialNumber) >= 0 ? balanceSerialNumber : logSerialNumber;
return result.add(BigDecimal.ONE);
}
lua脚本(放在项目resource目录下,名为redis_serial_number.lua文件)
思路:判断当前key值在redis中是否存在,存在则流水号加1并且返回,不存在则设置当前key,值为数据库查询出来的最新模块流水号
if
redis.call('get',KEYS[1])
then
return redis.call('incr',KEYS[1]);
else
return redis.call('set',KEYS[1],ARGV[1]);
end