您的位置:首页 > 数据库 > Redis

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();
}
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: