介绍redis分布式锁

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。

一、什么是分布式锁?

在我们写Java程序的时候,多线程争取同一个资源的时候,经常会使用到诸如syncchronize或Lock来实现锁操作,这种锁通常被称为“本地锁”。但是本地锁只能适用于在同一个进程内(同一个应用内的线程之间锁定资源),如果应用是分布式部署的,彼此之间是独立的进程,进程之间又存在需要争夺的资源,那么该如何对资源进行锁定?这就需要使用到分布式锁。

其实分布式锁和本地锁的基本原理是一样的,举个例子:上厕所

   4人去上厕所,厕所只有2个坑位

   先到坑位的人先占,占有后锁门(也就是上锁)

   后到的人没有占到坑位,只能等待

   先使用“坑位”的人,使用完资源,进行锁释放。

   锁释放之后,后到的人就可以获得坑位并上锁,如此循环往复。

上面的逻辑可以使用下面的代码来体现。

   @Resource

   RedisTemplate<String, String> redisTemplate;

   

   public void updateUserWithRedisLock(SysUser sysUser) throws InterruptedException {

     // 占分布式锁,去redis占坑

     Boolean lock = redisTemplate.opsForValue()

                                 .setIfAbsent("SysUserLock" + sysUser.getId(),  

                                   "value");

     if(lock) {

       //加锁成功... 执行业务

   

       redisTemplate.delete("SysUserLock" + sysUser.getId());   //删除key,释放锁

     } else {

       Thread.sleep(100);   // 加锁失败,重试

       updateUserWithRedisLock(sysUser);

     }

   }

setIfAbsent方法的作用是在某一个lock key不存在的时候,才能返回true;如果这个key已经存在了就返回false,返回false就是获取锁失败。setIfAbsent函数功能类似于redis命令行setnx。

二、分布式锁实现过程中的问题

问题一:异常导致锁没有释放

这个问题形成的原因就是程序在获取到锁之后,执行业务的过程中出现了异常,导致锁没有被释放。通俗的话说:上厕所的人死在了厕所里面,导致“坑位”资源死锁无法被释放。(当然这种情况出现的概率很小,但概率小不等于不存在。)

解决方案: 为redis的key设置过期时间,程序异常导致的死锁,在到达过期时间之后锁自动释放。也就说厕所门是电子锁,锁定的最长时间是有限制的,超过时长锁就会自动打开释放"坑位"资源。

           // 设置过期时间

    redisTemplate.expire("SysUserLock" + sysUser.getId(), timeout: 30, TimeUnit.SECONDS) ;

问题二:获取锁与设置过期时间操作不是原子性的

上文中我们虽然获取到锁,也设置了过期时间,看似完美。但是在高并发的场景下仍然会出问题,因为“获取锁”与“设置过期时间”是两个redis操作,两个redis操作不是原子性的。

可能出现这种情况:就在获取锁之后,设置过期时间之前程序宕机了。锁被获取到了但没有设置过期时间,最后又成为死锁。

解决方案: 获取锁的同时设置过期时间

   // 1. 分布式锁占坑

   Boolean lock = redisTemplate.opsForValue().setIfAbsent("SysUserLock" + sysUser.getId(), "value", 30, TimeUnit.SECONDS);

问题三:锁过期之后被别的线程重新获取与释放

这个问题出现的场景是:假如某个应用集群化部署存在多个进程实例,实例A、实例B。实例A获取到锁,但是执行过程超时了(数据库层面或其他层面导致操作执行超时)。超时之后锁被自动释放了,实例B获取到锁,并执行业务程序,执行完成之后把锁删除了。

   实际上这里还涉及到一个锁的续期的问题,我们后续再说,我们先来看下锁的释放的问题。

解决方案: 在释放锁之前判断一下,这把锁是不是自己的那一把,如果是别人的锁你就不要动。怎么判断这把锁是不是自己的?加锁时为value赋随机值,加锁的随机值等于解锁时的获取到的值,才能证明这把锁是你的。代码如下:

问题四:锁的释放不是原子性的

大家仔细看代码,锁的释放时三个操作,这三个操作不是原子性的。也就是说在高并发的场景下,你刚get到的redis key有可能也被别的线程get了,你刚要删除别的线程可能已经把这个key删除了。

为了解决这个问题,我们可以使用redis lua脚本(lua脚本是在一个事务里面执行的,可以保证原子性)。在Java代码中可以以字符串的形式存在。

   String script =  

    "if redis.call('get', KEYS[1]) == ARGV[1]  

     then return redis.call('del', KEYS[1])  

    else  

     return 0  

    end";

问题五:其他的问题?

上面我们分析了很多使用redis实现分布式锁可能出现的问题及解决方案,其实在实际的开发应用中还会有更多的问题。比如:

   目前我们的程序获取不到锁,就无限的重试,是不是应该在重试一定的次数之后就抛出异常?在有限的时间内通过异常给用户一个友好的响应。比如:程序太忙,请您稍后再试!

   程序A没有执行完成,锁定的key就过期了。虽然过期之后会自动释放锁,但是我的程序A的确没有执行完成啊,也没有异常抛出,就是执行的时间比较长,这个时候是不是应该对锁定的key进行续期?

   笔者对于分布式锁自动续期的这个功能也不是特别感冒,我觉得程序超过了我们设置的过期时间(比如说60s)一定是出现了问题,如果不是离线大数据批处理,一个程序执行60秒还没完成那一定是出问题了,你给我抛出异常就可以了。对于一个出问题的程序一直续期和死锁没什么区别。

所以实现一个分布式锁,不是我们想的那么简单,在高并发的环境下需要考虑的问题会复杂得多。怎么办?实际上分布式锁的细节时间有很多的现成的解决方案,不用我们去自己实现。比较完整优秀的分布式锁实现包括:

   RedisLockRegistry是spring-integration-redis中提供redis分布式锁实现类

   基于Redisson实现分布式锁原理(Redission是一个独立的redis客户端,是与Jedis、Lettuce同级别的存在)

对比

   RedisLockRegistry通过本地锁(ReentrantLock)和redis锁,双重锁实现;Redission通过Netty Future机制、Semaphore (jdk信号量)、redis锁实现。

   RedisLockRegistry和Redssion都是实现的可重入锁。(可重入锁是什么?下节再说)

   RedisLockRegistry对锁的刷新没有处理(续期),Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。

下面的章节,笔者为大家介绍RedisLockRegistry实现分布式锁,RedisLockRegistry作为spring-integration-redis与Spring Boot、Spring Data Redis无缝集成,不用自己做封装,简单易用。


相关文章
|
4月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
362 2
|
4月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
298 6
|
5月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
3月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
233 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
3月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
5月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
215 8
|
6月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
1685 7
|
缓存 NoSQL Java
为什么分布式一定要有redis?
1、为什么使用redis 分析:博主觉得在项目中使用redis,主要是从两个角度去考虑:性能和并发。当然,redis还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis。
1497 0
|
机器学习/深度学习 缓存 NoSQL
|
7月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?