SpringCloud在Redis中执行Lua脚本,避免并发所致id重复问题

问题背景

应公司产品要求,为了方便观察,编号需要由统一前缀+日期+连续增长序列构成。

方案

采用redis作为中间件从而产生唯一增长序列后缀。

伪代码大概如下:(线程不安全)

if(exists(key)) {incr(key);
} else {
	// 设置初始值为1000,并设置过期时间为凌晨0点加随机整数
	// 增加随机数是为了避免大量key同时过期,导致redis短暂不可用问题
③	SETEX key 过期时间 1000
}

发现问题

  1. 项目为内部系统,平时使用并不集中,因此一直没有出现过问题
  2. 由于每天凌晨需要定时获取昨日的司机运费数据,统一汇总并存储,项目在测试环境观察数据时发现产生了多个一模一样的1000这个id号

分析问题

  1. 线程到达①处代码时,向redis询问有无key,得到的结果为false,同时将此结果存储在本地内存
  2. 当执行③处代码时,线程被切换了,此线程休眠
  3. 第二个线程执行了③处代码,向redis设置了初始值,并获取到了编号1000
  4. 第一个线程被调度执行的时候又重复执行了③处代码,再次向redis设置了初始值,并获取到了编号1000,这时就出现了id重复问题了

解决方案

  1. 锁—简单粗暴,由于是SpringCloud项目,所以只能使用分布式锁。然而代价就是原本此处只有在每天初次使用时才需要加锁,其它时间使用redis的incr命令已经可以保证原子性了,如果采用分布式锁的方案就会做n-1次无用功,代价太大了
  2. 使用Lua脚本,在redis中执行具有天然的原子性特性,推荐

在SpringBoot中实现

  1. 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 
  1. 代码调用
stringRedisTemplate.execute(new DefaultRedisScript<Long>(SCRIPT, Long.class), keys, args);

注意事项

returnType 参数不能使用Integer.class ,需要使用Long.class。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章