使用Redis的几种线程安全的方式

2023-12-25 18:51:52 浏览数 (2)

场景

我经常使用Redis,比如有一个常见的场景就是获取key的值,如果小于某个阈值,就加一并且将加一后的值重新set回redis,返回true,否则返回false。就这样简单额场景,其中也牵扯到线程安全的问题。

摊牌了,其实一些复杂的与Redis交互业务逻辑用LUA脚本可以保证原子性。

源码下载

Demooo/springboot-demo/src/main/java/com/example/redisthreadsafe at master · cbeann/Demooo · GitHub

线程不安全举例

下面的代码基本就是大众的逻辑,但是有些代码在并发情况下,就会出现错误。以下面代码为例子,如果请求超过阈值LIMIT=10,请求就返回0。

现在考虑这样的一种的一种情况,两个线程同时第一次访问该接口,即大家到步骤2的时候num都是0,那么同时继续往下,那是不是这两个线程执行完毕后,你却发现redis里值为1 ,这就出现了线程不安全的问题。

代码语言:javascript复制
 @GetMapping("/notThreadSafe")
    public Object notThreadSafe() {

        //步骤1:拼接key
        int foodId = 1;
        String key = "stock:"   foodId;
        //阈值3
        final int LIMIT = 10;


        //步骤2:获取redis里存的值
        String s = stringRedisTemplate.opsForValue().get(key);
        int num = 0;
        if (!StringUtils.isEmpty(s)) {
            num = Integer.parseInt(s);
        }

        //步骤3:主判断逻辑
        if (num < LIMIT) {
            num  ;
            stringRedisTemplate.opsForValue().set(key, String.valueOf(num));
            return 1;
        }

        return 0;


    }

加锁synchronized

单实例线程安全没有问题,多实例还是数据不一致。

代码语言:javascript复制
@GetMapping("/singleInstanceThreadSafe")
public Object notThreadSafe() {

    //步骤1:拼接key
    int foodId = 1;
    String key = "stock:"   foodId;
    //阈值3
    final int LIMIT = 10;


   synchronized (this){
       //步骤2:获取redis里存的值
       String s = stringRedisTemplate.opsForValue().get(key);
       int num = 0;
       if (!StringUtils.isEmpty(s)) {
           num = Integer.parseInt(s);
       }

       //步骤3:主判断逻辑
       if (num < LIMIT) {
           num  ;
           stringRedisTemplate.opsForValue().set(key, String.valueOf(num));
           return 1;
       }

       return 0;
   }


}

加分布式锁:伪代码

参考:基于redis的分布式锁_CBeann的博客-CSDN博客

加锁的问题就是性能低,具有排他性

程安全实例:基于Lua脚本

lua脚本,所有的命令为原子性

代码语言:javascript复制
--根据key判断是否存在
local key = redis.call("EXISTS", KEYS[1])
--存在key
if tonumber(key) == 1 then
    --获取key的值
    local number = redis.call("GET", KEYS[1])
    --key的值小于阈值
    if tonumber(number) < tonumber(ARGV[1]) then
        redis.call("incrby", KEYS[1], ARGV[2])
        return 1
    else
        return 0
    end

else
    --不存在
    redis.call("SET", KEYS[1], ARGV[2])
    return 1
end

Java代码

代码语言:javascript复制
@GetMapping("/threadSafe")
    public Object threadSafe() {

        //步骤1:拼接key
        int foodId = 1;
        String key = "stock:"   foodId;
        //阈值3
        final int LIMIT = 10;


        DefaultRedisScript<Object> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(Object.class);
        defaultRedisScript.setScriptText("--根据key判断是否存在n"
                  "local key = redis.call("EXISTS", KEYS[1])n"
                  "--存在keyn"
                  "if tonumber(key) == 1 thenn"
                  "    --获取key的值n"
                  "    local number = redis.call("GET", KEYS[1])n"
                  "    --key的值小于阈值n"
                  "    if tonumber(number) < tonumber(ARGV[1]) thenn"
                  "        redis.call("incrby", KEYS[1], ARGV[2])n"
                  "        return 1n"
                  "    elsen"
                  "        return 0n"
                  "    endn"
                  "n"
                  "elsen"
                  "    --不存在n"
                  "    redis.call("SET", KEYS[1], ARGV[2])n"
                  "    return 1n"
                  "end");

        List<String> keys = new ArrayList<>();
        keys.add(key);
        Object[] args = new Object[2];
        args[0] = LIMIT;//阈值
        args[1] = 2;//每次加几

        Object execute = redisTemplate.execute(defaultRedisScript, keys, args);
        System.out.println(execute);




        return execute;


    }

0 人点赞