ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Redis分布式锁实战:从手写setnx到Redisson看门狗全解析

Redis分布式锁实战:从手写setnx到Redisson看门狗全解析 做分布式开发这几年绕不开的一个话题就是分布式锁。只要系统一拆成多个服务或者一个服务部署多个实例原本Java里一个synchronized就能解决的并发问题立刻变成了“怎么跨进程协调”的问题。网上讲Redis分布式锁的文章很多但要么只给一段setnx代码要么直接扔一个Redisson依赖然后说“这样就行”原理和坑基本不讲。这篇我把自己的实操经验完整梳理一遍从最原始的手写锁一步步演进到生产级方案讲清楚每一步为什么这么改Redisson的看门狗到底怎么回事主从切换为什么会导致锁丢失以及在Spring Boot里落地时最容易踩的序列化和超时问题。内容适合刚开始接触分布式锁的开发者也适合准备面试想系统梳理这块知识点的朋友。1. 分布式锁的本质为什么需要它又为什么选Redis1.1 没有分布式锁会发生什么先看一个最典型的场景商品秒杀库存只有10件同时有1000个请求打过来。如果服务是单机部署代码里用synchronized或者ReentrantLock锁住扣库存的逻辑一切正常。但现在的服务基本都是多实例部署请求通过负载均衡分发到不同机器此时每台机器上的synchronized只能锁住自己这个进程A机器和B机器上的线程会同时读到库存10同时扣减最后库存变成负数。有人会说数据库更新语句本身是原子的用乐观锁不行吗可行但有两个问题一是每次都要重试在高并发下数据库压力很大二是很多需要加锁的场景并不是简单的“update库存”而是多步骤操作比如“检查库存-扣减-生成订单-通知下游”这种跨步骤的原子性无法靠单条SQL保证。再比如定时任务。订单超时未支付需要关单这个任务在多个实例上都会触发。如果没加锁每台机器都会扫描一次未支付订单可能同一笔订单被关闭两次极端情况下还会重复发通知。分布式锁就是为了解决这种跨进程、跨实例的互斥问题同一时刻只能有一个实例执行关键代码。1.2 Redis做分布式锁的底气与边界分布式锁的实现方案不止Redis一种常见的有数据库唯一索引、ZooKeeper临时顺序节点、etcd的Lease机制以及Redis的SETNX命令。为什么Redis是大多数团队的首选核心就三点快、简单、生态好。Redis是纯内存操作单次锁获取的延迟通常在毫秒甚至亚毫秒级这在很多高并发场景下是硬指标。相比之下ZooKeeper的写入要经过ZAB协议的多节点确认etcd也依赖Raft共识性能都比Redis低一个量级。而且Redis的SET key value NX EX seconds命令本身是原子的一条命令就能完成“加锁设置过期时间”语义非常清晰几乎没有学习成本。整个Java生态里又有Redisson这种成熟的客户端库把可重入、自动续期、阻塞等待都封装好了写代码的效率很高。但Redis分布式锁有一个天然的短板它本质上依赖AP模型可用性优先在主从切换或集群分区时有可能出现锁丢失。这个后面专门用一章讲清楚这里先记住一个结论如果业务场景对“锁绝对不能丢”有强要求比如资金类操作Redis锁不够需要换成基于共识算法的方案如果只是防止重复执行、控制并发流量Redis锁完全够用这也是绝大多数业务场景。2. 第一版手写分布式锁从setnx到Lua脚本的演进2.1 最原始的双命令版本错在哪很多入门资料提到的第一版锁是这样的// 伪代码存在严重问题 if (jedis.setnx(lock:order, 1) 1) { // 加锁成功执行业务 doSomething(); // 释放锁 jedis.del(lock:order); }这段代码的问题一眼就能看出来如果doSomething()执行过程中抛了异常del根本执行不到锁就永远不释放其他线程全部卡死。有些资料会说“在finally里释放锁不就行了”但还是不够——如果服务在执行业务的瞬间宕机了连finally都来不及执行锁还是无法释放。所以分布式锁的第一原则出现了加锁时一定要设置过期时间让锁在最坏情况下也能自动释放避免死锁。基于这个思路第二版长这样jedis.setnx(lock:order, 1); jedis.expire(lock:order, 30); // 30秒后自动过期2.2 set nx ex原子操作把过期时间焊上去第二版看起来解决了死锁问题但引入了一个新的隐患setnx和expire是两条独立的命令不是原子的。如果setnx执行成功之后、expire执行之前服务宕机了锁照样不会过期问题回到原点。Redis在2.6.12版本之后对SET命令做了增强支持NX和EX参数可以一条命令完成加锁和设置过期时间SET lock:order 1 NX EX 30NX表示只有当key不存在时才设置Not eXistsEX表示设置过期时间单位秒。这条命令是原子的要么成功设置且30秒后自动过期要么什么都不做。这是手写分布式锁的第一个正确姿势。对应到Java代码用Jedis大概这样写String result jedis.set(lock:order, 1, NX, EX, 30); if (OK.equals(result)) { try { doSomething(); } finally { jedis.del(lock:order); } }2.3 删除锁时防止误删为什么必须用Lua脚本第三版已经能应付很多场景了但还有一个隐蔽的坑误删别人的锁。设想这样一个时间线线程A加锁成功key的值是A过期时间30秒。线程A执行业务超过了30秒锁自动过期了。线程B加锁成功key的值是B。线程A终于执行完执行jedis.del(lock:order)结果把线程B的锁删了。线程B刚拿到锁还没开始干活锁就没了其他线程趁虚而入互斥性完全失效。这个问题在后文讲Redisson时会发现它有自己的处理方式但手写锁必须自己解决。解决办法也很简单加锁时给value设置一个唯一标识比如UUID释放锁时先比较value是不是自己的是才删除。但“比较”和“删除”又是两步操作中间依然可能出问题所以必须把这两个动作合并成一个原子操作用Lua脚本实现if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本先在Redis服务端执行整个比较和删除过程不会被其他客户端插入保证原子性。Java调用方式差不多是这样String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; jedis.eval(script, Collections.singletonList(lock:order), Collections.singletonList(lockValue));到这里为止一个最小可用的Redis分布式锁就完整了加锁用SET NX EXvalue存唯一标识释放锁用Lua脚本先比对再删除。原理清楚了后面看Redisson源码才不至于一头雾水。2.4 完整的最小可用代码与测试把上面的思路整理成一个可运行的类import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; public class RedisLock { private static final String LOCK_KEY_PREFIX lock:; private static final int DEFAULT_EXPIRE_SECONDS 30; private final Jedis jedis; public RedisLock(Jedis jedis) { this.jedis jedis; } // 加锁线程内传requestId用来标识持有者 public boolean tryLock(String lockName, String requestId, int expireSeconds) { String result jedis.set(LOCK_KEY_PREFIX lockName, requestId, SetParams.setParams().nx().ex(expireSeconds)); return OK.equals(result); } // 释放锁Lua脚本保证“先校验再删除”的原子性 public boolean releaseLock(String lockName, String requestId) { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Object result jedis.eval(script, 1, LOCK_KEY_PREFIX lockName, requestId); return Long.valueOf(1).equals(result); } }测试时开多个线程抢一把锁每个线程持有锁1秒后释放打印日志可以看到同一时刻只有一个线程能进入临界区。这里还要重点说明一下实际生产里不建议直接用这种手写版本因为还有锁自动续期的问题没解决业务执行时间超过过期时间怎么办这个在Redisson章节展开。但手写的意义在于理解原理面试时手撕代码也是这个版本。3. Redisson把分布式锁做到开箱即用3.1 为什么不在生产里手写锁手写锁有个绕不开的痛点过期时间设多长设太短业务还没执行完锁就过期了其他线程趁虚而入设太长万一持有锁的实例宕机了其他线程要等很久才能拿到锁系统的可用性下降。生产环境里一个方法的执行时间很难准确预估有时候网络抖动一下、下游接口慢一点原本100毫秒的活可能要跑10秒固定过期时间怎么设置都不合适。Redisson的解决方案是“看门狗”WatchDog机制。简单说加锁成功后如果锁没有指定leaseTime持有时间Redisson会启动一个后台定时任务每隔一段时间检查当前线程是否还持有锁如果还持有就自动把过期时间延长一直到业务执行完释放锁为止。这样既避免了锁自己过期导致并发问题又不会因为业务执行太久而死锁。3.2 看门狗机制锁超时和业务跑不完的矛盾怎么解Redisson加锁默认的leaseTime是30秒看门狗每10秒钟leaseTime / 3续期一次把锁的有效期重新刷回30秒。这个“每10秒检查一次”的间隔不是拍脑袋定的它要保证一个周期内至少能续约多次防止网络抖动导致续约失败时锁很快过期。看门狗的核心实现逻辑在RedissonLock类的renewExpiration()方法里本质就是一段定时任务调Lua脚本更新过期时间。这里有几个关键点如果用lock.lock()没有传leaseTime会启动看门狗自动续期。如果用lock.lock(10, TimeUnit.SECONDS)或者tryLock(10, 5, TimeUnit.SECONDS)里指定了leaseTimeRedisson不会启动看门狗锁到了时间就自动释放。所以生产上有一个很常见的建议如果能预估业务执行时间就显式传leaseTime不依赖看门狗这样逻辑更可控如果业务时间不确定或者偶尔会出现大流量导致方法执行特别慢就依赖看门狗。实际项目中后一种情况更多。还有一点容易被忽略看门狗只有在当前这个JVM实例还活着、当前线程的运行状态还正常时才有效。如果持有锁的实例发生了长时间的Full GC分钟级JVM暂停导致看门狗线程无法续期锁依然会过期其他线程还是能拿到锁也就是“GC pause引发的锁失效”。这是所有“基于租约续期”的分布式锁的通病不光Redis有。对绝大多数业务来说这种极端概率可以接受。3.3 可重入、tryLock、公平锁常用API怎么选Redisson的锁实现是RLock接口用法和JUC的ReentrantLock很像Resource private RedissonClient redissonClient; public void closeOrder(String orderId) { RLock lock redissonClient.getLock(lock:order: orderId); boolean locked false; try { // 第一个参数最大等待时间拿不到锁时最多等多久 // 第二个参数锁的持有时间leaseTime // 第三个参数时间单位 locked lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { // 拿不到锁做降级处理比如返回“系统繁忙” return; } doCloseOrder(orderId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } } }注意这个tryLock(3, 10, TimeUnit.SECONDS)前面说过传了leaseTime10秒就不会启动看门狗到点自动释放。这种写法适合能明确预估执行时间的场景。如果换成lock.lock()则使用默认30秒leaseTime并启动看门狗。Redisson还提供了可重入锁和公平锁。可重入是默认行为同一个线程可以多次加同一把锁锁内部有一个计数器记录重入次数释放一次递减一次减到0才真正删除key。这个实现用Redis的Hash结构完成的键是锁名字段是线程标识值是重入次数。如果面试官追问“Redisson可重入是怎么实现的”回答思路就是把RLock内部对同一个线程的记录次数当成计数器。公平锁用redissonClient.getFairLock(name)获取内部基于Redis的有序集合ZSet实现排队确保锁按照请求顺序分配。但这个公平锁吃性能高并发下不建议用绝大多数业务不需要这种强公平性默认的“不公平锁”已经能满足互斥需求。4. 深入原理主从切换丢锁、RedLock与共识算法4.1 主从异步复制为什么会导致锁丢失现在来看Redis分布式锁被讨论最多、面试也最常被追问的“丢锁”问题。假设你的Redis是一主一从或一主多从的标准架构。所有写命令都打到主节点主节点通过异步复制把命令同步到从节点。线程A在主节点上加锁成功主节点还没把这条写入命令复制给从节点主节点突然宕机。哨兵进行故障转移某台从节点被提升为新的主节点。因为之前那条加锁命令没有复制到这台从节点新主节点上根本没有这把锁的key。此时线程B尝试加锁自然加锁成功A和B同时进入了临界区锁的互斥性就失效了。这个问题的根源是Redis主从复制是异步的加锁结果尚未同步到从节点时主节点就宕机了故障转移完成之后锁就“凭空消失”。严格来说这不是Redis锁“有问题”而是AP系统在一定约束下牺牲了一致性。换句话讲Redis锁保证的是“在绝大多数时间锁是对的”但无法保证“任何时刻锁都是绝对安全的”。4.2 RedLock真能解决问题吗优缺点分析为了应对主从切换丢锁Redis作者antirez提出了RedLock算法部署N个完全独立不互相复制、不共享数据的Redis实例通常N取奇数比如5个。加锁时向所有实例发送加锁命令只有过半数的实例比如5个里至少3个加锁成功且总耗时小于锁的有效期此时才视为成功释放锁时向所有实例发送释放命令。RedLock的设计思路是即使其中一两个实例宕机只要多数派还持有锁就能保证互斥。这个算法刚推出时被广泛认可但在2016年分布式系统专家Martin Kleppmann发表了一篇著名的文章质疑RedLock核心观点有三个第一RedLock仍然依赖时钟假设。它假设各个实例的时钟不会发生大跳变一旦某台机器的时钟发生跳跃比如运维手动调时间、NTP同步异常可能会导致锁的过期时间计算失衡锁还是可能失效。第二RedLock需要多个Redis实例同时可用可用性比单实例更低一个实例挂掉还好多个实例挂掉或网络分区时锁可能完全不可用。第三即便RedLock解决了主从切换的丢锁也无法解决GC pause带来的问题持有锁的线程发生长时间STW停止世界锁过期了其他线程拿到锁原线程恢复后根本不知道自己的锁已经过期。antirez随后进行了回应双方的核心分歧在于“分布式锁到底要达到什么级别的安全性才算合格”。我的经验是RedLock的问题在学术界讨论得很热闹但在工程领域尤其在国内互联网公司真正落地RedLock的团队非常少。原因很简单它需要维护多个互不相关的Redis实例成本高、可用性低而大部分业务来一句“Redis挂了一个实例就够我喝一壶了还用5个实例”就足以把这个方案否决掉。如果你负责的确实是资金级、强一致性的场景应该考虑ZooKeeper或etcd而不是RedLock如果只是防止重复执行、控制并发单机Redis加Redisson已经足够主从切换丢锁的概率在实际生产中极低。4.3 要不要换成ZooKeeper或etcd凡是问“Redis分布式锁安不安全”的场景最后基本都会绕到“要不要换ZooKeeper”。先说结论如果你的核心诉求是“锁一定不能丢”那就换。ZooKeeper实现锁的原理是临时顺序节点客户端在指定目录下创建临时顺序节点然后检查自己是不是序号最小的那个是则获得锁否则监听比自己序号小的那个节点的删除事件等它释放后再次尝试。这个方案的优点是利用ZAB协议保证了多副本之间的强一致性一旦节点创建成功其他客户端立刻能看到客户端崩溃后临时节点会自动消失也不会死锁。听起来很完美但代价是性能和复杂度。ZooKeeper的写请求要经过Leader选举、日志复制、过半确认延迟比Redis高一个量级而且客户端还要处理Session超时、Watcher机制等一堆细节代码量明显增加。etcd的情况类似基于Raft共识协议提供Lease和Revision来实现分布式锁一致性同样有保证但也不是没有代价etcd集群的部署和维护本身就比Redis复杂得多。我的个人判断标准是涉及资金、积分、防重提交等强一致性要求的场景直接用ZooKeeper或etcd别犹豫其他大部分业务场景Redis Redisson已经是最优解别为了理论“绝对安全”引入庞大的基础设施。很多时候“分布式锁够不够安全”这个问题如果产品说不出损失到底多严重那大概率就是够了。5. 实战Spring Boot集成分布式锁的完整配置5.1 工程依赖与基础配置项目里最常用的组合是Spring Boot 2.x / 3.x Redisson依赖只需要一个redisson-spring-boot-starter版本要和Spring Boot匹配。以Spring Boot 2.7为例dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.17.7/version /dependency然后在application.yml里配置spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0如果Redis部署方式是主从、哨兵或集群可以用Redisson的独立配置方式Configuration public class RedissonConfig { Bean public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(yourpassword); return Redisson.create(config); } }单机、哨兵、集群分别对应useSingleServer()、useSentinelServers()、useClusterServers()三种配置入口。从Config对象的结构就能看出Redisson对不同部署形态的适配做了很细的封装切换成本很低。5.2 序列化问题为什么key不能乱用RedisTemplate这个坑我进公司第二年踩过一次印象非常深。项目引入了Spring Data Redis的RedisTemplate没有做任何配置就直接用结果分布式锁的key在Redis客户端里看到的一堆乱码而且同一个业务里一个地方用RedisTemplate加锁另一个地方用StringRedisTemplate释放锁怎么都释放不掉。原因在于Spring Data Redis默认使用JDK序列化器JdkSerializationRedisSerializer会把key和value都序列化成二进制字节流在Redis Desktop Manager里面看到的就是\xAC\xED\x00\x05t...这类乱码。而StringRedisTemplate使用字符串序列化器两者序列化后的key完全相同但存到Redis后字节不一致导致看起来是同一个key实际上是两个不同的key。所以项目里如果要用RedisTemplate操作字符串类型的key最稳妥的做法是指定字符串序列化器Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // key 使用字符串序列化方便在 Redis 客户端里直接查看 StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); // value 可以使用 JSON 序列化推荐 Jackson GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }还有一个小问题值得单独提一下网上有个很常见的报错“使用RedisTemplate的increment()报错:ERR value is not an integer or out of range”。这通常是因为value当初被JDK序列化器存成了二进制字节再用increment()做整数自增Redis发现取出来的不是整数直接抛错。解决办法也是统一序列化方式确保value存的是可解析的整数形式。5.3 一个完整的下单扣库存案例结合实际业务来看一个典型的下单扣库存场景用Redisson做分布式锁应该这么写Service public class OrderService { Resource private RedissonClient redissonClient; Resource private StockMapper stockMapper; public void createOrder(Long skuId, Integer count) { // 锁的粒度尽量小不同商品锁不同key减少锁竞争 String lockKey lock:stock: skuId; RLock lock redissonClient.getLock(lockKey); boolean locked false; try { // 最多等2秒10秒后锁自动释放 locked lock.tryLock(2, 10, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(系统繁忙稍后重试); } // 1. 查询库存 Integer stock stockMapper.selectStockForUpdate(skuId); if (stock count) { throw new BusinessException(库存不足); } // 2. 扣减库存SQL 中使用 stock stock - #{count} 条件库存大于等于 count int rows stockMapper.deductStock(skuId, count); if (rows 0) { throw new BusinessException(库存扣减失败); } // 3. 生成订单写订单表 stockMapper.insertOrder(skuId, count); } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } } } }这里有两个实操要点一个是锁的粒度lock:stock:{skuId}比lock:stock按商品维度拆开并发能力完全不同另一个是Redis锁和本地数据库事务的配合如果deductStock和insertOrder在同一个本地事务里锁的范围要能覆盖整个事务不然会出现锁释放了、事务还没提交其他请求读到旧数据的问题。当然更严谨的做法是把扣库存和锁的释放放到事务提交之后但实现复杂度会上升要根据实际业务权衡。5.4 阻塞等待与锁持有的最佳实践分布式锁的合理持有时间没有一个标准答案但有几个通用原则一是锁的持有时间要远小于业务可容忍的等待时间。比如用户下单接口要求2秒内返回那么tryLock的等待时间上限就不应该超过这个阈值等不到就直接降级。二是leaseTime尽量不要设置成“永远”。即使有看门狗也最好给锁设置一个上限比如60秒防止业务出现死循环或长时间阻塞时锁一直不释放拖着不健康的实例还占着锁。三是一旦确定拿不到锁要快速失败不要无限重试。网上有些示例代码是while(true) { tryLock... }死循环抢锁生产环境这么写流量一上来就能把Redis打成热点还会造成线程堆积、内存暴涨。正常的降级策略包括直接返回失败、异步排队处理、本地限流保护。6. 高可用与运维主从哨兵、持久化和安全防护6.1 Redis实例怎么部署才靠谱分布式锁的可靠性很大程度取决于Redis本身的高可用架构。常见的三种形态主从模式、哨兵模式、集群模式。主从模式就是一台主库带一台或多台从库主库负责读写从库负责备份但不会自动故障转移。主库挂了需要人工把从库提升为主库这个过程中断时间较长不适合承载关键锁服务。哨兵模式在主从基础上增加了Sentinel组件会持续监控主节点检测到主节点故障时自动执行故障转移选一个从节点提升为新主节点。这是生产环境里最常见的Redis高可用形态它能挡住大多数硬件故障场景。集群模式Redis Cluster把数据分片到多个节点每个节点又有自己的从节点适合数据量大、读写并发高的场景。如果只是用分布式锁数据量不大集群模式不是必须的甚至因为涉及分片锁的key如果不经过哈希约定可能出现跨节点访问增加网络开销。我的经验是锁服务这种低频但高可用的数据用哨兵模式就很好如果公司已经统一部署了集群也可以直接复用。用Docker快速验证环境的话一条命令就能起一个带密码的Redis实例docker run -d --name redis-lock \ -p 6379:6379 \ -v /data/redis/conf:/usr/local/etc/redis \ -v /data/redis/data:/data \ redis:7.0 redis-server /usr/local/etc/redis/redis.conf注意这里单独挂载了conf目录配置文件里需要设置appendonly yes开启AOF持久化以及requirepass设置访问密码。6.2 持久化配置与锁可靠性的关系Redis的持久化方式主要有两种RDB快照和AOF追加日志。很多人以为Redis持久化只是为了“数据不丢”跟分布式锁没关系。其实关系很大如果Redis主节点没有开启持久化主节点重启后内存数据全部丢失所有分布式锁的key跟着消失这种情况就相当于“全量丢锁”。即使开启了RDB也有丢数据的窗口因为RDB默认是周期性生成快照比如save 900 1表示“900秒内有1个key变化就生成快照”如果锁写入后还没触发快照宕机重启锁就没了。AOF的fsync策略有always、everysec和no三种always每执行一次写命令同步一次最安全但性能开销最大everysec每秒同步一次最多丢1秒数据性能和安全性平衡生产环境用得最多。所以对分布式锁这种数据量小但安全敏感的场景建议开启AOF且至少配置appendfsync everysec如果业务对锁的可靠性要求再高一点可以单独为锁所在的Redis实例开启appendfsync always用一点性能损耗换更强的数据安全。6.3 未授权访问等安全风险怎么自查Redis分布式锁在公网环境下的安全隐患非常突出。早几年很多Redis裸奔在公网端口直接暴露没有设置密码攻击者连上去之后可以用CONFIG SET dir /root/.ssh配合CONFIG SET dbfilename authorized_keys写入公钥直接拿到服务器权限这就是常说的Redis未授权访问漏洞。自查方法很简单检查Redis配置文件里protected-mode是否为yes是否设置了requirepass端口是否只对可信来源开放以及是否禁止了CONFIG命令可以重命名为空字符串来禁用。生产环境的Redis服务器至少应该做到下面几点protected-mode yes避免默认配置下可以被外部任意连接。requirepass设置强密码客户端连接必须带AUTH。用安全组或防火墙限制6379端口只允许应用服务器网段访问不暴露公网。不要用root用户启动Redis一旦Redis被利用至少不会直接拿到root权限。如果使用容器注意不要把Redis端口映射到宿主机的公网IP上。平时用Redis Desktop Manager、Another Redis Desktop Manager这类可视化客户端连接测试时也需要确认这些客户端属于可信人员并且生产环境最好走SSH隧道或内网访问别直接开公网映射。7. 面试与避坑分布式锁高频考点和个人经验7.1 高频面试题速览根据我最近几年面试候选人和被面试的经验关于Redis分布式锁的高频问题基本集中在下面几块第一类是原理类。比如“Redis分布式锁是怎么实现的”“底层命令是什么”。回答要点就是SET key value NX EX加锁、Lua脚本释放锁、value存唯一标识能顺手提一句原子性就够了。第二类是进阶类。“锁过期了业务还没执行完怎么办”。这块是区分带宽度的分水岭能答出Redisson看门狗机制的基本就有加分如果能补充“看门狗只在本进程内定时续期实例发生长时间GC时锁可能失效”说明真的读过源码。第三类是权衡类。“Redis分布式锁和ZooKeeper分布式锁有什么区别”。回答方向可以从它们背后的模型出发Redis是AP系统性能高、主从切换时可能丢锁ZooKeeper是CP系统一致性有强保证但性能和运维复杂度不如Redis。落到场景上就是业务能接受偶发的锁失效就选Redis资金级项目就选ZooKeeper/etcd。第四类是设计类。“如果让你设计一个分布式锁你的思路是什么”。这题其实是在考察对整个过程的掌握能讲清楚加锁、过期时间、唯一标识、释放锁的原子操作、自动续期、主从切换丢失、以及降级策略基本就及格了。7.2 常见坑位清单把我在真实项目里踩过、也看别人踩过的坑整理成了一张速查表坑位现象正确做法加锁和设置过期时间非原子服务宕机后死锁用SET key value NX EX释放锁直接DEL误删其他线程的锁Lua脚本先比对value再删除锁过期但业务没跑完两个线程同时进入临界区用Redisson看门狗自动续期锁的key没做序列化配置Redis客户端看到乱码、释放失败统一用String序列化器锁粒度太粗不同业务互相阻塞吞吐量低按业务维度拆分锁key拿不到锁就死循环重试打爆Redis线程大量堆积tryLock设置最大等待时间快速失败只加锁不设leaseTime业务死循环时锁永久持有设置合理leaseTime或依赖看门狗最后一条单独解释一下Redisson的lock.lock()如果一直不释放看门狗会无限续期锁永远在。这种“永不自愈”的设计在某些场景下是灾难所以工程项目里更推荐给leaseTime一个上限或者结合业务超时熔断机制。7.3 写在最后的实战体会从手写setnx到引入Redisson整个演进过程本质上是在回答一个问题在分布式环境下怎么用最小成本换最大的互斥保证。Redis分布式锁的方案之所以普及不是因为它理论完美而是因为它足够简单、足够快能覆盖绝大多数业务场景。真要追绝对安全RedLock不是银弹ZooKeeper和etcd才是更接近“绝对安全”的解但它们的复杂度和运维成本也应该被认真评估。如果让我给刚入手的团队一条最实用的建议那就是默认直接用Redisson不要重复造轮子但要能看懂Redisson在做什么不然出了问题连排查思路都没有。手写锁只适合学习原理和面试场景生产环境里序列化、看门狗、公平锁、读写锁这些细节自己从头实现既费时又容易出bug成熟的客户端库踩过的坑一定比自己多得多。顺便说一个我在生产环境里的小习惯所有加锁的地方日志里都要打出锁的key、持有者标识、等待耗时和加锁结果。分布式锁出的问题往往是多实例、多线程、多时间线叠加之后的复杂问题没有日志根本无从下手。有了这些日志大部分锁相关的事故都能在上线后的几分钟内定位而不是翻半天代码也找不到“为什么锁没生效”。这也是我从几次凌晨紧急排查中总结出来的最重要的经验。
返回列表