Redis結合LUA腳本實現序列號唯一引發的問題

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();
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章