这里写自定义目录标题
问题背景
应公司产品要求,为了方便观察,编号需要由统一前缀+日期+连续增长序列构成。
方案
采用redis作为中间件从而产生唯一增长序列后缀。
伪代码大概如下:(线程不安全)
① if(exists(key)) {
② incr(key);
} else {
// 设置初始值为1000,并设置过期时间为凌晨0点加随机整数
// 增加随机数是为了避免大量key同时过期,导致redis短暂不可用问题
③ SETEX key 过期时间 1000
}
发现问题
- 项目为内部系统,平时使用并不集中,因此一直没有出现过问题
- 由于每天凌晨需要定时获取昨日的司机运费数据,统一汇总并存储,项目在测试环境观察数据时发现产生了多个一模一样的
1000
这个id号
分析问题
- 线程到达①处代码时,向redis询问有无key,得到的结果为false,同时将此结果存储在
本地内存
中 - 当执行③处代码时,线程被切换了,此线程休眠
- 第二个线程执行了③处代码,向redis设置了初始值,并获取到了编号1000
- 第一个线程被调度执行的时候又重复执行了③处代码,再次向redis设置了初始值,并获取到了编号1000,这时就出现了id重复问题了
解决方案
- 锁—简单粗暴,由于是SpringCloud项目,所以只能使用分布式锁。然而代价就是原本此处只有在每天初次使用时才需要加锁,其它时间使用redis的
incr
命令已经可以保证原子性了,如果采用分布式锁的方案就会做n-1次无用功,代价太大了 - 使用Lua脚本,在redis中执行具有天然的原子性特性,推荐
在SpringBoot中实现
- Lua脚本
if redis.call('EXISTS', KEYS[1]) == 1
then
return redis.call('INCR', KEYS[1])
else
redis.call('SETEX', KEYS[1], ARGV[1], ARGV[2])
return tonumber(ARGV[2])
end
- 代码调用
stringRedisTemplate.execute(new DefaultRedisScript<Long>(SCRIPT, Long.class), keys, args);
注意事项
returnType 参数不能使用Integer.class ,需要使用Long.class。