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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 场景 我经常使用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 ,这就出现了线程不安全的问题。


 @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


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


@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;
   }
}


加分布式锁:伪代码


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


程安全实例:基于Lua脚本


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


--根据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代码


@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"
                + "--存在key\n"
                + "if tonumber(key) == 1 then\n"
                + "    --获取key的值\n"
                + "    local number = redis.call(\"GET\", KEYS[1])\n"
                + "    --key的值小于阈值\n"
                + "    if tonumber(number) < tonumber(ARGV[1]) then\n"
                + "        redis.call(\"incrby\", KEYS[1], ARGV[2])\n"
                + "        return 1\n"
                + "    else\n"
                + "        return 0\n"
                + "    end\n"
                + "\n"
                + "else\n"
                + "    --不存在\n"
                + "    redis.call(\"SET\", KEYS[1], ARGV[2])\n"
                + "    return 1\n"
                + "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;
    }
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3月前
|
监控 NoSQL 安全
如何在 Redis 中正确使用多线程?
【10月更文挑战第16天】正确使用 Redis 多线程需要综合考虑多个因素,并且需要在实践中不断摸索和总结经验。通过合理的配置和运用,多线程可以为 Redis 带来性能上的提升,同时也要注意避免可能出现的问题,以保障系统的稳定和可靠运行。
70 2
|
3月前
|
存储 NoSQL Redis
Redis 新版本引入多线程的利弊分析
【10月更文挑战第16天】Redis 新版本引入多线程是一个具有挑战性和机遇的改变。虽然多线程带来了一些潜在的问题和挑战,但也为 Redis 提供了进一步提升性能和扩展能力的可能性。在实际应用中,我们需要根据具体的需求和场景,综合评估多线程的利弊,谨慎地选择和使用 Redis 的新版本。同时,Redis 开发者也需要不断努力,优化和完善多线程机制,以提供更加稳定、高效和可靠的 Redis 服务。
72 1
|
11天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
34 1
|
4月前
|
消息中间件 存储 NoSQL
剖析 Redis List 消息队列的三种消费线程模型
Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。
109 20
剖析 Redis List 消息队列的三种消费线程模型
|
3月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
46 1
|
3月前
|
NoSQL Redis 数据库
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
本文解释了Redis为什么采用单线程模型,以及为什么Redis单线程模型的效率和速度依然可以非常高,主要原因包括Redis操作主要访问内存、核心操作简单、单线程避免了线程竞争开销,以及使用了IO多路复用机制epoll。
62 0
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
|
4月前
|
NoSQL 网络协议 Unix
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
102 1
|
4月前
|
存储 消息中间件 NoSQL
Redis的单线程设计之谜:高性能与简洁并存
Redis的单线程设计之谜:高性能与简洁并存
51 1
|
6月前
|
消息中间件 缓存 NoSQL
Redis快速度特性及为什么支持多线程及应用场景
Redis快速度特性及为什么支持多线程及应用场景
132 11
|
5月前
|
缓存 开发框架 NoSQL
【Azure Redis 缓存】Azure Redis 异常 - 因线程池Busy而产生的Timeout异常问题
【Azure Redis 缓存】Azure Redis 异常 - 因线程池Busy而产生的Timeout异常问题