Redis结合LUA脚本实现序列号唯一引发的问题
2018-04-01 17:05
633 查看
Redis结合LUA脚本实现序列号唯一引发的问题
背景
项目中使用redis结合lua脚本来获取序列号,保证序列号的唯一,lua脚本是我在网上找的,看好多大神都在用,也就觉得没问题,直接引入了自己的项目。脚本内容如下(本人对脚本内容添加了注释,方便读者理解):-- 获取最大的序列号,样例为16081817202494579 -- 从redis中获取到的序列如果小于传入的序列号,就把redis中的序列号置为当前序列号,并返回给调用者 -- 从redis中获取到的序列如果大于传入的序列号,就按照增长规则递增,并返回给调用者 -- 通过这样的方式保证序列号的唯一性 local function get_max_seq() //KEYS[1]:第一个参数代表存储序列号的key 相当于代码中的业务类型 local key = tostring(KEYS[1]) //KEYS[2]:第二个参数代表序列号增长速度 local incr_amoutt = tonumber(KEYS[2]) //KEYS[3]:第三个参数为序列号 (yyMMddHHmmssSSS + 两位随机数) local seq = tostring(KEYS[3]) //序列号过期时间大小 local month_in_seconds = 24 * 60 * 60 * 30 //Redis的 SETNX 命令可以实现分布式锁,用于解决高并发 //如果key不存在,将 key 的值设为 seq,设置成成功返回1 未设置返回0 //若给定的 key 已经存在,则 SETNX 不做任何动作。 if (1 == redis.call('setnx', key, seq)) then //设置key的生存时间 为 month_in_seconds秒 redis.call('expire', key, month_in_seconds) //将序列返回给调用者 return seq else //key值存在,直接获取key值大小(序列号) local prev_seq = redis.call('get', key) //获取到的序列号 小于 当前序列号 if (prev_seq < seq) then //直接将key值设为当前序列号 redis.call('set', key, seq) //返回给调用者 return seq else //获取到的序列号 大于 当前序列号 就将key值置为key+incr_amoutt redis.call('incrby', key, incr_amoutt) //将key+incr_amoutt 返回给调用者 return redis.call('get', key) end end end return get_max_seq()
在脚本中可以使用redis.call函数调用Redis命令
在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句则默认返回null
脚本优化
项目业务功能在不断扩展,redis中存放的数据也越来越多,为了更加方便的管理redis中的key,我对脚本内容进行了修改,将原有的key-value存储修改为key-hashkey-value形式存储,修改后的脚本:local function get_max_seq() local key = 'SEEKER:SEQ:BIZ' local increment = 1 local hkey = tostring(KEYS[1]) local seq = tostring(KEYS[2]) local month_in_seconds = 24 * 60 * 60 * 30 if (1 == redis.call('hsetnx', key, hkey, seq)) then redis.call('expire',key,month_in_seconds) return seq else local prev_seq = redis.call('hget',key, hkey) if(prev_seq < seq) then redis.call('hset',key,hkey,seq) return seq else redis.call('hincrby', key, hkey, increment) return redis.call('hget', key, hkey) end end end return get_max_seq()
高并发导致获取序列号重复
使用修改后的脚本,项目也稳定运行了半年多时间,突然有一天,运维跟我说获取的序列号重复了。于是我本地环境模拟高并发开始测试脚本,即每次传入脚本的seq参数都是固定字符串,结果获取到序列号有重复的,脚本的确有问题。经研究:脚本中在比较字符串大小时,使用的是tostring,比较结果不准确,可能出现’24’ > ‘25’情况(具体脚本为什么不能用tostring进行比较,请读者自行查阅资料),应该使用tonumber,于是再次对脚本进行了修改。修改后脚本内容如下:
local function get_max_seq() local key = tostring(KEYS[1]) local increment = tonumber(KEYS[2]) local hkey = tostring(KEYS[3]) local seq = tonumber(KEYS[4]) local month_in_seconds = 2592000 if (1 == redis.call('hsetnx', key, hkey, seq)) then redis.call('expire',key,month_in_seconds) return seq else local prev_seq = redis.call('hget',key, hkey) if(tonumber(prev_seq) < seq) then redis.call('hset',key,hkey,seq) return seq else return redis.call('hincrby', key, hkey, increment) end end end return get_max_seq()
使用修改后的脚本再进行高并发测试,序列号不会重复,问题已经解决。
总结
任何新技术的引用,都要仔细研究,亲身测试。附:测试代码(springboot实现)
package com.seeker.controller; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.StringUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.EncodedResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.DefaultScriptExecutor; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.util.FileCopyUtils; /** * @title * @description * @since Java8 */ @Component public class RedisUtil { private static StringRedisTemplate redisStringTemplate; private static RedisScript<String> redisScript; private static DefaultScriptExecutor<String> scriptExecutor; private RedisUtil(StringRedisTemplate template) throws IOException { RedisUtil.redisStringTemplate = template; // 初始化lua脚本调用 的redisScript 和 scriptExecutor ClassPathResource luaResource = new ClassPathResource("get_next_seq.lua"); EncodedResource encRes = new EncodedResource(luaResource, "UTF-8"); String luaString = FileCopyUtils.copyToString(encRes.getReader()); redisScript = new DefaultRedisScript<>(luaString, String.class); scriptExecutor = new DefaultScriptExecutor<>(redisStringTemplate); } public static String getBusiId(String type) { List<String> keyList = new ArrayList<>(); keyList.add("24"); keyList.add("23"); String seq = scriptExecutor.execute(redisScript, keyList); return type + seq; } }
package com.seeker.controller; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.Vector; import java.util.concurrent.CountDownLatch; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.PostMethod; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.fasterxml.jackson.databind.ObjectMapper; /** * * @author Fan.W * @since 1.8 */ @Controller @RequestMapping("/seeker") public class TestController { private static Vector<String> s = new Vector<>(); @Autowired private StringRedisTemplate template; @RequestMapping(value = "/redistest") public String redistest() { CountDownLatch startSignal = new CountDownLatch(1); for (int i = 0; i < 100; ++i) { new Thread(new Task(startSignal)).start(); } startSignal.countDown(); return "hello"; } class Task implements Runnable { private final CountDownLatch startSignal; Task(CountDownLatch startSignal) { this.startSignal = startSignal; } public void run() { try { String seq = RedisUtil.getBusiId("24"); System.out.println(seq); if (s.contains(seq)) { System.out.println("重复id " + seq); } else { s.add(seq); } } catch (Exception e) { e.printStackTrace(); } } } }
相关文章推荐
- 使用lua脚本和jedis实现redis的hmsetnx命令,操作hash表时不覆盖原有数据
- Php+Redis 实现Redis提供的lua脚本功能
- Redis 2.6 Lua脚本功能实现分析
- redis实现附近的人,但jedis中没有相关api,那么直接使用lua脚本执行。
- Nginx 内嵌lua脚本,结合Redis使用
- Redis使用lua脚本实现increase + expire 的原子操作
- 简介Lua脚本与Redis数据库的结合使用
- redis与lua脚本的结合使用
- redis哨兵模式使用lua脚本实现分布式锁
- Nginx 内嵌lua脚本,结合Redis使用
- 为什么在 Redis 实现 Lua 脚本事务?
- Redis 使用 Lua 实现 split 结合 HMGET 批量读取数据
- redis与lua脚本结合,java基本使用
- 基于Redis Lua脚本实现的分布式锁 | 日拱一卒
- Redis系列四 - 在springboot中通过Lua脚本在redis中实现定时任务
- Xcopy结合脚本,实现文件服务器自动备份
- GridView 结合ymPrompt脚本实现的确认是否删除操作
- C语言获取开机时间(结合VBS脚本实现语音输出)
- 用mysql+redis实现微博feed架构上需要注意哪些问题
- https,https的本地测试环境搭建,asp.net结合https的代码实现,http网站转换成https网站之后遇到的问题