不用找了,基于 Redis 的分布式锁实战来了!

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了。乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞。

前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,


基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了。乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞。


那么,本文主要来讨论基于redis的分布式锁算法问题。

从2.6.12版本开始,redis为SET命令增加了一系列选项(set [key] NX/XX EX/PX [expiration]):


EX seconds – 设置键key的过期时间,单位时秒


PX milliseconds – 设置键key的过期时间,单位时毫秒


NX – 只有键key不存在的时候才会设置key的值


XX – 只有键key存在的时候才会设置key的值


原文地址:https://redis.io/commands/set

中文地址:http://redis.cn/commands/set.html


注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。


这里简单提一下,在旧版本的redis中(指2.6.12版本之前),使用redis实现分布式锁一般需要setNX、expire、getSet、del等命令。而且会发现这种实现有很多逻辑判断的原子操作以及本地时间等并没有控制好。


而在旧版本的redis中,redis的超时时间很难控制,用户迫切需要把setNX和expiration结合为一体的命令,把他们作为一个原子操作,这样新版本的多选项set命令诞生了。然而这并没有完全解决复杂的超时控制带来的问题。


接下来,我们的一切讨论都基于新版redis。


在这里,我先提出几个在实现redis分布式锁中需要考虑的关键问题


1、死锁问题;

1.1、为了防止死锁,redis至少需要设置一个超时时间;


1.2、由1.1引申出来,当锁自动释放了,但是程序并没有执行完毕,这时候其他线程又获取到锁执行同样的程序,可能会造成并发问题,这个问题我们需要考虑一下是否归属于分布式锁带来问题的范畴。


2、锁释放问题,这里会有两个问题;

2.1、每个获取redis锁的线程应该释放自己获取到的锁,而不是其他线程的,所以我们需要在每个线程获取锁的时候给锁做上不同的标记以示区分;


2.2、由2.1带来的问题是线程在释放锁的时候需要判断当前锁是否属于自己,如果属于自己才释放,这里涉及到逻辑判断语句,至少是两个操作在进行,那么我们需要考虑这两个操作要在一个原子内执行,否者在两个行为之间可能会有其他线程插入执行,导致程序紊乱。


3、更可靠的锁;

单实例的redis(这里指只有一个master节点)往往是不可靠的,虽然实现起来相对简单一些,但是会面临着宕机等不可用的场景,即使在主从复制的时候也显得并不可靠(因为redis的主从复制往往是异步的)。


关于Martin Kleppmann的Redlock的分析


原文地址:https://redis.io/topics/distlock

中文地址:http://redis.cn/topics/distlock.html


文章分析得出,这种算法只需具备3个特性就可以实现一个最低保障的分布式锁。


安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。


活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。


活性B(Liveness property B): 容错。只要大部分Redis节点都活着,客户端就可以获取和释放锁.


我们来分析一下:

第一点安全属性意味着悲观锁(互斥锁)是我们做redis分布式锁的前提,否者将可能造成并发;


第二点表明为了避免死锁,我们需要设置锁超时时间,保证在一定的时间过后,锁可以重新被利用;


第三点是说对于客户端来说,获取锁和手动释放锁可以有更高的可靠性。


更进一步分析,结合上文提到的关键问题,这里可以引申出另外的两个问题:

怎么才能合理判断程序真正处理的有效时间范围?(这里有个时间偏移的问题)


redis Master节点宕机后恢复(可能还没有持久化到磁盘)、主从节点切换,(N/2)+1这里的N应该怎么动态计算更合理?


接下来再看,redis之父antirez对Redlock的评价


原文地址:http://antirez.com/news/101


文中主要提到了网络延迟和本地时钟的修改(不管是时间服务器或人为修改)对这种算法可能造成的影响。


最后,来点实践吧

I、传统的单实例redis分布式锁实现(关键步骤)

获取锁(含自动释放锁):

SET resource_name my_random_value NX PX 30000  
 手动删除锁(Lua脚本):  
if redis.call("get",KEYS[1]) == ARGV[1] then  
    return redis.call("del",KEYS[1])  
else  
    return 0  
end  

II、分布式环境的redis(多master节点)的分布式锁实现

为了保证在尽可能短的时间内获取到(N/2)+1个节点的锁,可以并行去获取各个节点的锁(当然,并行可能需要消耗更多的资源,因为串行只需要count到足够数量的锁就可以停止获取了);


另外,怎么动态实时统一获取redis master nodes需要更进一步去思考了。


QA,补充一下说明(以下为我与朋友沟通的情况,以说明文中大家可能不够明白的地方):


1、在关键问题2.1中,删除就删除了,会造成什么问题?


线程A超时,准备删除锁;但此时的锁属于线程B;线程B还没执行完,线程A把锁删除了,这时线程C获取到锁,同时执行程序;所以不能乱删。


2、在关键问题2.2中,只要在key生成时,跟线程相关就不用考虑这个问题了吗?


不同的线程执行程序,线程之间肯虽然有差异呀,然后在redis锁的value设置有线程信息,比如线程id或线程名称,是分布式环境的话加个机器id前缀咯(类似于twitter的snowflake算法!),但是在del命令只会涉及到key,不会再次检查value,所以还是需要lua脚本控制if(condition){xxx}的原子性。


3、那要不要考虑锁的重入性?


不需要重入;try…finally 没得重入的场景;对于单个线程来说,执行是串行的,获取锁之后必定会释放,因为finally的代码必定会执行啊(只要进入了try块,finally必定会执行)。


4、为什么两个线程都会去删除锁?(貌似重复的问题。不管怎样,还是耐心解答吧)


每个线程只能管理自己的锁,不能管理别人线程的锁啊。这里可以联想一下ThreadLocal。


5、如果加锁的线程挂了怎么办?只能等待自动超时?


看你怎么写程序的了,一种是问题3的回答;另外,那就自动超时咯。这种情况也适用于网络over了。


6、时间太长,程序异常就会蛋疼,时间太短,就会出现程序还没有处理完就超时了,这岂不是很尴尬?


是呀,所以需要更好的衡量这个超时时间的设置。


实践部分主要代码:


RedisLock工具类:

package com.caiya.cms.web.component;  
import com.caiya.cache.CacheException;  
import com.caiya.cache.redis.JedisCache;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
import java.util.Objects;  
import java.util.concurrent.TimeUnit;  
/**  
 * redis实现分布式锁  
 * 可实现特性:  
 * 1、使多线程无序排队获取和释放锁;  
 * 2、丢弃未成功获得锁的线程处理;  
 * 3、只释放线程本身加持的锁;  
 * 4、避免死锁  
 *  
 * @author wangnan  
 * @since 1.0  
 */  
public final class RedisLock {  
    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);  
    /**  
     * 尝试加锁(仅一次)  
     *  
     * @param lockKey       锁key  
     * @param lockValue     锁value  
     * @param expireSeconds 锁超时时间(秒)  
     * @return 是否加锁成功  
     * @throws CacheException  
     */  
    public static boolean tryLock(String lockKey, String lockValue, long expireSeconds) throws CacheException {  
        JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();  
        try {  
            String response = jedisCache.set(lockKey, lockValue, "nx", "ex", expireSeconds);  
            return Objects.equals(response, "OK");  
        } finally {  
            jedisCache.close();  
        }  
    }  
    /**  
     * 加锁(指定最大尝试次数范围内)  
     *  
     * @param lockKey       锁key  
     * @param lockValue     锁value  
     * @param expireSeconds 锁超时时间(秒)  
     * @param tryTimes      最大尝试次数  
     * @param sleepMillis   每两次尝试之间休眠时间(毫秒)  
     * @return 是否加锁成功  
     * @throws CacheException  
     */  
    public static boolean lock(String lockKey, String lockValue, long expireSeconds, int tryTimes, long sleepMillis) throws CacheException {  
        boolean result;  
        int count = 0;  
        do {  
            count++;  
            result = tryLock(lockKey, lockValue, expireSeconds);  
            try {  
                TimeUnit.MILLISECONDS.sleep(sleepMillis);  
            } catch (InterruptedException e) {  
                logger.error(e.getMessage(), e);  
            }  
        } while (!result && count <= tryTimes);  
        return result;  
    }  
    /**  
     * 释放锁  
     *  
     * @param lockKey   锁key  
     * @param lockValue 锁value  
     */  
    public static void unlock(String lockKey, String lockValue) {  
        JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();  
        try {  
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";  
            Object result = jedisCache.eval(luaScript, 1, lockKey, lockValue);  
//            Objects.equals(result, 1L);  
        } catch (Exception e) {  
            logger.error(e.getMessage(), e);  
        } finally {  
            jedisCache.close();  
        }  
//        return false;  
    }  
    private RedisLock() {  
    }  
}  

使用工具类的代码片段1:

        ...  
        String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId();// 跟业务相关的唯一拼接键  
        String lockValue = Constant.DEFAULT_CACHE_NAME + ":" + System.getProperty("JvmId") + ":" + Thread.currentThread().getName() + ":" + System.currentTimeMillis();// 生成集群环境中的唯一值  
        boolean locked = RedisLock.tryLock(lockKey, lockValue, 100);// 只尝试一次,在本次处理过程中直接拒绝其他线程的请求  
        if (!locked) {  
            throw new IllegalAccessException("您的操作太频繁了,休息一下再来吧~");  
        }  
        try {  
            // 开始处理核心业务逻辑  
            Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());  
            ...  
            ...  
        } finally {  
            RedisLock.unlock(lockKey, lockValue);// 在finally块中释放锁  
        }  

使用工具类的代码片段2:

        ...  
        String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId();  
        String lockValue = Constant.DEFAULT_CACHE_NAME + ":机器编号:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis();  
        boolean locked = RedisLock.lock(lockKey, lockValue, 100, 20, 100);// 非公平锁,无序竞争(这里需要合理根据业务处理情况设置最大尝试次数和每次休眠时间)  
        if (!locked) {  
            throw new IllegalAccessException("系统太忙,本次操作失败");// 一般来说,不会走到这一步;如果真的有这种情况,并且在合理设置锁尝试次数和等待响应时间之后仍然处理不过来,可能需要考虑优化程序响应时间或者用消息队列排队执行了  
        }  
        try {  
            // 开始处理核心业务逻辑  
            Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());  
            ...  
            ...  
        } finally {  
            RedisLock.unlock(lockKey, lockValue);  
        }  
        ...  

附加:

基于redis的分布式锁实现客户端Redisson:

https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

相关实践学习
基于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
目录
打赏
0
0
0
0
579
分享
相关文章
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
411 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
9天前
|
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
133 83
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
28 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
HarmonyOS Next 是华为新一代操作系统,专注于分布式技术的深度应用与生态融合。本文通过技术特点、应用场景及实战案例,全面解析其核心技术架构与开发流程。重点介绍分布式软总线2.0、数据管理、任务调度等升级特性,并提供基于 ArkTS 的原生开发支持。通过开发跨设备协同音乐播放应用,展示分布式能力的实际应用,涵盖项目配置、主界面设计、分布式服务实现及部署调试步骤。此外,深入分析分布式数据同步原理、任务调度优化及常见问题解决方案,帮助开发者掌握 HarmonyOS Next 的核心技术和实战技巧。
276 76
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
Redis,分布式缓存演化之路
本文介绍了基于Redis的分布式缓存演化,探讨了分布式锁和缓存一致性问题及其解决方案。首先分析了本地缓存和分布式缓存的区别与优劣,接着深入讲解了分布式远程缓存带来的并发、缓存失效(穿透、雪崩、击穿)等问题及应对策略。文章还详细描述了如何使用Redis实现分布式锁,确保高并发场景下的数据一致性和系统稳定性。最后,通过双写模式和失效模式讨论了缓存一致性问题,并提出了多种解决方案,如引入Canal中间件等。希望这些内容能为读者在设计分布式缓存系统时提供有价值的参考。感谢您的阅读!
107 6
Redis,分布式缓存演化之路
大道至简-基于ACK的Deepseek满血版分布式推理部署实战
本教程演示如何在ACK中多机分布式部署DeepSeek R1满血版。
|
1月前
|
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
44 9
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
鸿蒙技术分享:HarmonyOS Next 深度解析 随着万物互联时代的到来,华为发布的 HarmonyOS Next 在技术架构和生态体验上实现了重大升级。本文从技术架构、生态优势和开发实践三方面深入探讨其特点,并通过跨设备笔记应用实战案例,展示其强大的分布式能力和多设备协作功能。核心亮点包括新一代微内核架构、统一开发语言 ArkTS 和多模态交互支持。开发者可借助 DevEco Studio 4.0 快速上手,体验高效、灵活的开发过程。 239个字符
250 13
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
91 10
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等