ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式锁的4种主流实现:Redis、ZooKeeper、Etcd与数据库方案对比

分布式锁的4种主流实现:Redis、ZooKeeper、Etcd与数据库方案对比 做后端开发的迟早要碰一次分布式锁。不管你是用Spring一把梭写单体应用还是已经在微服务架构里摸爬滚打只要服务一拆、多实例一部署synchronized和Lock就管不住局面了——线程锁锁的是单个JVM进程内的资源跨进程、跨节点的那批共享资源必须换一套思路来锁。本文想聊的就是这件事分布式锁的4种主流实现方式。我会把这四种方案的核心原理、实现细节、常见坑位和选型依据全部摊开来讲包含可直接参考的配置、命令和代码片段覆盖大家在面试和实际项目中最高频遇到的那些问题。如果你是刚接触分布式锁的后端工程师或者正在为团队选型做技术调研这篇应该能帮你省掉不少翻文档的时间。1. 分布式锁到底锁的是什么先搞清楚需求再谈实现很多人一上来就盯着Redis还是ZooKeeper选型其实顺序反了。分布式锁本质是多个进程对同一临界资源的互斥访问控制但具体到业务里“临界资源”的形态千差万别。不先把需求想清楚后面选型一定纠结。1.1 哪些场景真的需要分布式锁最常见的一类是共享资源扣减。典型例子是秒杀库存商品库存存在数据库里十个订单服务实例同时收到下单请求每个实例都先查库存、再扣库存如果没有跨进程互斥两个请求可能同时读到剩余1件各自扣减成功超卖就这么来的。单体应用可以用数据库行锁解决但服务拆成多实例后行锁依然能兜底可缓存层、业务层的并发控制就需要分布式锁了。第二类是定时任务的重复执行问题。很多系统会用Scheduled在每个实例上跑同一个定时任务比如凌晨同步数据、生成报表。多个实例同时执行同一个任务轻则浪费资源重则产生重复数据。分布式锁保证同一时刻只有一个实例拿到执行权其他实例直接跳过这是非常典型的应用场景。第三类是接口幂等性控制。支付回调、消息消费这类场景同一个事件可能被投递多次如果用“先查状态再写记录”的方式判断是否处理过在并发情况下依然有漏洞。分布式锁可以确保同一个业务标识在同一时刻只有一个请求能进入处理逻辑后续重复请求直接拒绝或等待。第四类是缓存失效瞬间的保护。缓存击穿时大量请求同时回源数据库可以在回源逻辑上加锁只让一个线程去查库、重建缓存其他线程等待或直接拿旧值。这个场景里锁的粒度可以做得比较大因为回源本身耗时可控锁的可靠性要求也不算极致。1.2 一把合格的分布式锁要满足哪些条件加锁解锁看起来简单但细抠起来一条条列会很多。我这里按重要性排序说互斥性这是底线任何时刻只能有一个客户端持有锁。做不到互斥整个方案就是摆设。自动释放持有锁的节点宕机、网络分区、进程被kill锁必须在有限时间内自动释放否则整个系统会被一把死锁卡死。Redis靠过期时间兜底ZooKeeper靠临时节点特性兜底数据库方案要靠事务和超时机制兜底。可重入性同一个线程如果已经持有锁再次获取同一把锁应该直接成功而不是把自己挡住。分布式锁的可重入实现比单机锁麻烦需要记录持有线程和重入次数。高性能加锁解锁的开销要足够低不能在业务主链路上成为瓶颈。Redis锁的单次操作是微秒级数据库唯一索引插入在低并发下也还好ZooKeeper涉及到节点创建和watcher通知毫秒级高并发下吞吐差距明显。高可用锁服务本身不能成为单点。Redis主从切换会带来锁丢失问题ZooKeeper集群和Etcd集群在多数派存活时可用。公平性是否需要按照请求顺序获取锁。ZooKeeper和Etcd的方案天然公平按顺序节点/revisionRedis方案默认不公平随机抢占大多数业务不需要公平锁但少数场景如分布式任务调度会关注。注意不要指望一把分布式锁同时满足全部条件不同方案本质上是在一致性、可用性、性能之间做取舍。比如Redis锁性能最高但极端情况下会失效ZooKeeper锁可靠性强但吞吐有限。选型前先把“业务能容忍什么损失”定义清楚比挑技术方案更重要。2. 方案一基于数据库实现的分布式锁数据库方案是历史最悠久、实现最直白的一种也是我见过很多老系统里还在跑的方式。它不需要额外引入中间件直接复用现有MySQL实例适合对性能要求不高、并发量有限的内部系统。2.1 唯一索引实现insert和delete就是加锁和释放核心思路很简单建一张锁表在某几个关键字段上建唯一索引获取锁就是向表里插入一条记录释放锁就是删除这条记录。谁插入成功谁就拿到了锁。建表语句大致长这样CREATE TABLE distributed_lock ( id bigint(20) NOT NULL AUTO_INCREMENT, lock_key varchar(128) NOT NULL COMMENT 锁的唯一标识, owner_id varchar(64) NOT NULL COMMENT 持有者标识用于防止误删, expire_time datetime DEFAULT NULL COMMENT 过期时间兜底用, PRIMARY KEY (id), UNIQUE KEY uk_lock_key (lock_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;加锁逻辑就是往这张表插入一行。比如业务标识是order_pay_10086则执行INSERT INTO distributed_lock (lock_key, owner_id) VALUES (order_pay_10086, instance-123);如果插入成功说明拿到了锁可以继续执行业务逻辑。如果插入报Duplicate entry错误说明锁已被其他节点持有就轮询重试或直接失败。释放锁则是删除对应记录DELETE FROM distributed_lock WHERE lock_key order_pay_10086 AND owner_id instance-123;删除时带上owner_id是为了避免误删别人的锁——比如线程A持有锁后处理时间过长锁表里没有自动过期机制A宕机后锁记录还在后来线程B插入失败一直等不到锁释放这时如果用DELETE WHERE lock_key ?就会把别人的锁删掉。而带owner_id能保证只有持有者本人能删自己的锁。但这个方案有个致命问题没有自动过期。持有者宕机锁记录永远留在表里其他节点永远拿不到锁。所以实际项目中必须加一个定时清理任务定期扫描expire_time过期的记录并删除。这里expire_time的设置比较讲究设太短会导致业务还没执行完锁就被清理设太长会导致宕机后锁长时间不可用我的经验是设置为业务预估最大耗时的2到3倍再让清理任务每30秒跑一次。2.2 悲观锁方案select ... for update除了唯一索引数据库方案还可以用SELECT ... FOR UPDATE实现。它的思路是借助数据库自身的事务锁机制在一个事务里对某一行加行级排他锁事务结束才释放。BEGIN; SELECT * FROM distributed_lock WHERE lock_key order_pay_10086 FOR UPDATE; -- 执行业务逻辑... COMMIT;这个方案的优点是实现简单完全借助InnoDB的行锁能力不存在“忘记释放”的问题——事务提交或回滚都会释放锁。但它要求lock_key字段有索引否则FOR UPDATE会升级为表锁并发能力直接崩掉。缺点是性能和连接占用问题很明显。持有锁期间数据库连接一直被占用如果锁的持有时间稍长比如几百毫秒连接池很容易被打满。另外如果业务逻辑里出现了慢查询或长时间事务会拖垮整个数据库。这个方案我只建议在锁持有时间非常短几十毫秒以内、并发量极低的场景下使用。2.3 数据库方案的适用场景与明显短板数据库方案的最大优势是“零成本引入”不需要额外维护Redis、ZooKeeper这类中间件团队如果对数据库运维非常熟练可以快速实现一套。同时它天然有事务保证可以和业务操作绑定在同一个事务里一致性更好理解。短板也很突出性能瓶颈数据库写入和行锁的开销远大于Redis高并发下会产生大量锁等待和重试吞吐量上不去。单点风险单库部署本身是单点锁服务跟着数据库一起挂即使做了主从主从切换期间锁记录的一致性也难保证。没有自动续期机制业务执行时间超过预估后锁可能被定时清理或超时释放导致多个节点同时进入临界区互斥性被打破。数据库故障影响面大锁表和业务表混在同一个实例上一旦锁表出现死锁或锁等待可能拖累核心业务。我自己的结论是数据库锁适合那种“并发不大、不想引入新组件、对极端一致性要求不高”的内部管理系统比如后台管理工具的互斥操作、定时任务的防重。真正的核心交易链路我基本不用它宁可多花点成本上Redis。3. 方案二基于Redis的分布式锁Redis锁是当前互联网公司里用得最广的分布式锁方案没有之一。原因很简单性能高、实现简单、Redis几乎每个团队都已经在运维。但它也有很多容易踩的细节坑我需要从最小实现开始完整讲一遍。3.1 最小可用实现SET NX EX 两步合一最早很多人用SETNX加锁、EXPIRE设置过期时间两步操作但这样有个经典bugSETNX成功后进程崩溃EXPIRE没执行锁就永远不释放。现在推荐的做法是用一条命令同时完成加锁和过期时间设置SET lock:order_pay_10086 instance-123 NX EX 30参数含义NX只有当key不存在时才设置保证互斥性EX 30设置过期时间为30秒防止持有者宕机后锁不释放value instance-123这个value非常重要它标识锁的持有者释放锁时必须校验防止误删。释放锁时需要先检查value是否是自己再删除。这一步推荐用Lua脚本保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endJava侧的伪代码如下String lockKey lock:order_pay_10086; String ownerId UUID.randomUUID().toString(); // 加锁10秒过期 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, ownerId, Duration.ofSeconds(10)); if (Boolean.TRUE.equals(locked)) { try { // 执行业务逻辑 } finally { // 释放锁校验ownerId String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute( new DefaultRedisScript(script, Long.class), List.of(lockKey), ownerId); } }很多新手会问释放锁时为什么不能直接DEL举一个场景线程A拿到锁因为GC停顿或网络延迟业务执行超过了30秒锁自动过期线程B拿到同key的锁开始执行A终于苏醒执行结束后DEL——它删掉的是B持有的锁。B刚执行到一半锁没了线程C又拿到锁进来互斥性瞬间被打破。带ownerId校验能避免这种误删但不能避免A持锁超时这种根因问题。3.2 Redisson的看门狗机制解决续期问题上面那个GC停顿导致锁提前过期的问题业界通行的解法是自动续期。Redisson这个客户端库实现了类似看门狗的机制默认情况下加锁后如果业务没执行完后台会有一个定时任务每隔lockLeaseTime / 3默认约10秒检查一次锁是否还持有如果持有就把过期时间重置为30秒。这个过程不断重复直到业务执行完手动释放。这就是所谓“看门狗”带来的体验业务代码里不用手动设置过期时间锁会自动跟着业务执行时长续期。它的本质是起一个定时任务在后台续期业务结束时关闭定时任务并释放锁。使用Redisson时有一个细节要特别注意lock()和tryLock()默认会启动看门狗但如果你手动传入了leaseTime参数看门狗就不会启动锁会在你指定的时间后强制过期。我见过不少人传了leaseTime又以为锁会自动续期结果长任务在锁过期后变成多人同时执行。所以要么不传leaseTime用默认看门狗要么自己保证业务能在leaseTime内完成。可重入方面Redisson也做了支持同一个线程重复lock()同一把锁时内部会维护一个ThreadLocal计数器每进入一次加1每释放一次减1减到0才真正删除key。这个设计跟JDK的ReentrantLock一致保证同一个线程的重入不会被自己的锁挡住。3.3 Redlock算法与主从切换的争议Redis锁一个经常被面试官追问的点是锁在Redis主从架构下会丢吗答案是会。默认的复制是异步的A在主节点SET NX EX成功此时从节点还没来得及复制这条数据主节点挂了哨兵把从节点提升为新主节点新主节点上没有这把锁另一个客户端就能加同一把锁成功互斥性被破坏。为了解决这个问题Redis官方提出了Redlock算法向多个独立的Redis节点一般5个依次尝试加锁每个节点设置一个比锁总生命周期短的超时时间只有在超过一半节点加锁成功且总耗时小于锁的生存时间时才认为加锁成功。释放时向所有节点发送释放命令。这个算法理论上更可靠但业界对它的争议一直很大。以Martin Kleppmann为代表的一派认为Redlock在复杂的网络分区场景下依然无法保证绝对安全而且它会引入额外的延迟和运维成本。我的实际建议是绝大多数业务的并发问题不需要用到Redlock这种强一致方案。Redis锁的优点是快如果需要绝对强一致应该直接换ZooKeeper或Etcd而不是在Redis上堆算法。Redlock更适合Redis多节点部署、业务能容忍极小概率锁丢失、但如果加锁失败会造成严重后果的场景这种场景其实很少。提示Redis锁适用的底色是“容忍极小概率的锁失效”。选它等于承认在极端情况主从切换、长时间GC下可能出现两个客户端同时持有锁。如果你的业务是扣钱、转账这类绝对不能并发的要么给Redis方案做好补偿机制如数据库的唯一约束兜底要么直接上ZooKeeper。4. 方案三基于ZooKeeper的分布式锁ZooKeeper做的分布式锁在可靠性和公平性上比Redis强很多代价是性能不如Redis、运维也更重。很多做互联网金融、交易系统的团队会把关键链路的锁放到ZK上靠它强一致的特性兜底。4.1 临时顺序节点与Watcher机制ZK的锁模型建立在两个核心特性上临时节点和顺序节点。临时节点创建节点的客户端会话结束时ZK会自动删除该节点。这天然解决了“持有者宕机后锁不释放”的问题不需要像Redis那样依赖过期时间。顺序节点同一父节点下创建节点时ZK会自动在名字后面追加一个单调递增的序号。这个序号天然给请求排了队公平锁的“先来后到”就靠它实现。加锁的流程大致这样所有客户端在同一个父节点下创建临时顺序节点比如/locks/order_pay_10086/lock_0000000001。然后每个客户端获取父节点下的全部子节点列表判断自己的序号是不是最小的。如果最小说明拿到锁如果不是就监听排在自己前面那个节点的删除事件并进入等待状态。当前一个节点被删除说明前一个持有者释放了锁自己再去判断一次是否轮到自己。所谓“监听前一个节点”而不是“监听全部节点”是为了避免羊群效应。如果每个客户端都监听整个父节点的变化一旦锁释放所有等待者全部被唤醒大家同时去判断自己是否最小瞬间产生大量并发请求。而只监听前一个节点时锁释放只唤醒一个节点整体的通知量和ZK集群压力都会小很多。4.2 基于Curator封装的使用示例直接操作ZK原生API写分布式锁非常繁琐要处理重连、会话过期、节点监听异常等等。生产环境建议直接用Curator官方封装它把分布式锁的细节全部屏蔽掉了API简单到看一遍就能上手。CuratorFramework client CuratorFrameworkFactory.newClient( zk1:2181,zk2:2181,zk3:2181, new RetryNTimes(3, 1000)); client.start(); InterProcessMutex lock new InterProcessMutex( client, /locks/order_pay_10086); lock.acquire(10, TimeUnit.SECONDS); try { // 执行业务逻辑 } finally { lock.release(); }这个InterProcessMutex默认就是公平锁且支持可重入。Curator内部还提供了InterProcessReadWriteLock支持读写锁区分读读不互斥、读写互斥、写写互斥。这在一些“读多写少”的分布式资源管理里非常有用比如配置中心的配置发布和客户端读取。Curator有一个需要注意的参数是sessionTimeout它决定客户端与ZK之间的会话超时时间。这个值设置过短网络抖动会导致会话过期锁被自动释放业务还在执行引发并发问题设置过长客户端真正故障后锁要很久才能释放阻塞其他节点。一般建议设置为10到30秒并且要配合JVM的GC停顿情况来权衡。另外ZK客户端重连后底层持有的锁对象会自动重建还是需要重新获取Curator的封装已经处理了大部分但实际运行中还是要靠单测和故障注入来验证。4.3 ZK锁的优缺点与适用场景ZK锁最大的优点是可靠性高临时节点保证持有者会话结束自动释放锁没有Redis那种“锁过期时间没设好导致永久死锁”的问题顺序节点天然提供公平性不存在两个客户端同时抢到锁的确定性错误ZK集群通过ZAB协议保证多数派数据一致主从切换时锁的数据不容易丢。缺点是性能相对较低。每次加锁解锁都涉及ZK节点创建和删除以及一次或多次网络RTT吞吐量在每秒几千次级别跟Redis的每秒几万到十几万次不在一个数量级。另外ZK集群本身要维护3到5个节点监控、扩容、故障处理都需要专门的运维知识相比Redis的普及度ZK的学习和运维成本更高。适用场景我总结为三类对一致性和可靠性要求极高的核心链路、读写锁等复杂锁语义需要原生支持、业务并发量不算极高但绝不允许出错的锁场景。如果你们团队已经把ZK当做注册中心或配置中心在用那直接复用ZK做分布式锁几乎零额外成本那么用它比额外引入Etcd更顺手。5. 方案四基于Etcd的分布式锁Etcd是云原生生态里的后起之秀和Kubernetes深度绑定很多新项目会直接选择它作为配置中心和注册中心。Etcd内部用Raft协议保证强一致性而且API设计比ZK更现代做分布式锁也是它的常见用法之一。5.1 租约与RevisionEtcd锁的两个核心概念Etcd实现分布式锁的核心依赖两个机制Lease租约和Revision版本号。租约Etcd支持创建一个带TTL的租约把key关联到这个租约上。租约会定期自动续期不会因为客户端不操作就过期但如果客户端崩溃、网络断开或主动撤销租约关联的key会被自动删除。这一点非常像ZK的临时节点但实现方式更优雅。RevisionEtcd的每一次写操作都会分配一个全局递增的revision。同一个前缀下创建的keyrevision越大说明创建越晚。判断是否持有锁本质上就是看自己的revision是不是当前前缀下最小的。加锁的过程大致是客户端创建一个特定前缀下的key例如/locks/order_pay_10086/key值带上自己的标识并关联一个租约然后通过Txn事务查询该前缀下的所有key拿到自己的revision如果自己的revision是最小的就拿到锁否则监听revision比自己小的、最大的那个key的删除事件。5.2 Etcd锁的完整逻辑与代码示意用Go的clientv3库写Etcd锁思路如下cli, _ : clientv3.New(clientv3.Config{ Endpoints: []string{http://etcd1:2379, http://etcd2:2379, http://etcd3:2379}, DialTimeout: 5 * time.Second, }) defer cli.Close() // 1. 创建租约TTL 10秒 leaseResp, _ : cli.Grant(context.TODO(), 10) leaseID : leaseResp.ID // 2. 在租约上创建key lockKey : /locks/order_pay_10086/ fmt.Sprintf(%x, time.Now().UnixNano()) cli.Put(context.TODO(), lockKey, holder-instance-1, clientv3.WithLease(leaseID)) // 3. 获取前缀下所有key按revision排序 resp, _ : cli.Get(context.TODO(), /locks/order_pay_10086/, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)) // 4. 判断自己的revision是否最小 for _, kv : range resp.Kvs { if string(kv.Key) lockKey { // 我是最小的拿到锁 break } // 否则监听前一个key的删除事件 }不需要每条都自己实现。Etcd官方在contrib/recipes目录下提供了sync.RWMutex风格的分布式锁封装直接用就行这个库和ZK的Curator一样帮我们屏蔽了租约续期、节点监听这些细节。它同样支持RWMutex的读写锁语义。Etcd锁的续期机制很有意思客户端持有租约后需要周期性地通过KeepAlive请求续租否则租约到期后key被自动删除。这个设计和Redis的看门狗不同Redis的续期是客户端本地的定时任务Etcd的续约是客户端和Etcd服务端之间有真实的心跳请求。两者比下来Etcd的续约更可控因为服务端清楚租约是否仍然活跃而Redis只能靠key的过期时间推断。5.3 Etcd与ZK的对比以及适用场景Etcd锁和ZK锁在分布式锁的实现思路上几乎一致都是“先创建带自动清理的节点/key再按序号排队”。区别主要在底层一致性协议和客户端生态上。对比项EtcdZooKeeper一致性协议RaftZABKey过期机制租约 定时续约临时节点随会话结束删除实现锁的方式前缀目录 revision排序父节点下临时顺序节点客户端生态gRPC风格Cloud Native生态好Curator功能丰富老牌稳定与K8s集成天然集成无性能较高Raft多副本写入中等ZAB写入性能略低如果你的团队已经在用Kubernetes和Etcd再引入一个ZK就明显不划算Etcd一个组件就够用了。如果团队已经有成熟的ZK集群在支撑Dubbo或其他系统那么沿用ZK也完全合理。两者选谁更多取决于你们的基础设施现状而不是技术本身的高低。6. 四种方案横向对比与选型建议前面讲完四种方案接下来的内容会很直接把它们的各项能力放在同一张表里做对比并给出我实际选型时的一套判断逻辑。6.1 一张表看懂四种方案的差异维度数据库RedisZooKeeperEtcd实现复杂度简单建表/事务简单SET NX EX中等Curator封装中等官方recipe性能低高中中高可靠性低单点无自动过期中主从切换会丢锁高高公平性不支持默认不支持天然公平天然公平可重入需自行设计需用Redisson等库Curator支持recipe支持读写锁不支持需自行设计Curator支持recipe支持自动释放机制无、需定时清理过期时间临时节点租约额外运维成本无低Redis常用高中典型场景内部管理低频互斥高并发缓存/幂等控制强一致核心链路云原生基础设施表格里“可靠性”一栏要解释一下。Redis那行写“中”不是说Redis锁不好而是它在主从切换、长时间GC等极端场景下存在锁丢失的可能。数据库那行写“低”是因为它既没有自动过期又受制于单库的稳定性。ZK和Etcd都靠多数派协议保证强一致只要集群多数节点存活锁数据就不会丢。6.2 选型决策路径与我的建议选型不要上来就看技术社区风向先把业务的硬约束列出来第一步问自己能不能容忍锁极端情况下失效。如果能容忍极小概率的并发问题比如生成报表、缓存回源、非支付类幂等控制直接选Redis理由是性能好、落地快、团队基本都会。如果不能容忍比如支付、转账、资金流水优先ZK或Etcd。第二步看团队已有的基础设施。如果Redis已经在线上了没有特别理由就不要为了一把锁引一个新组件如果ZK已经用于Dubbo注册中心那ZK锁顺手就行如果项目跑在Kubernetes上Etcd本来就在部署一套Etcd锁也不会有额外运维包袱。数据库锁只在“并发极低、绝对不能新增依赖、锁时长很短”时考虑。第三步评估锁的使用频率和持有时间。高频短时锁比如缓存重建Redis优势明显低频但持有时间不确定的长任务比如大数据批处理任务ZK或Etcd的自动释放机制更让人放心因为你不用焦虑“过期时间定多长才合适”。我自己在项目里用得最多的是Redis Redisson覆盖了绝大部分业务场景。只有在资金相关、必须强一致的极少量接口上我才会单独用ZK或Etcd给它们加一道锁并且还会在数据库层面做唯一索引兜底。这种“多层兜底”的思想比把全部安全性寄托在某一种锁上要实在得多。7. 高频面试问答与实测排坑记录最后这部分全是干货前半部分整理面试中最高频的分布式锁问题及回答方向后半部分是我在实际项目中踩过的坑和修复经验。7.1 面试官常问的5个问题第一问Redis分布式锁如何保证释放锁时的原子性回答要点释放锁不能简单DEL要先用GET比较value是否为当前持有者标识再DEL。这两个操作必须原子执行否则存在“判断通过后、删除前锁被其他线程抢走”的时间窗口。标准做法是用Lua脚本把比较和删除合成一个原子操作Redisson也是这么做的。第二问Redis锁过期了业务还没执行完怎么办回答要点方案有两个方向。一是引入看门狗自动续期机制Redisson默认用后台定时任务每隔leaseTime/3续期一次二是业务代码里尽量缩短锁持有时间把不必要的外部RPC调用移出临界区。无论哪种都要想清楚极端情况下的兜底手段。第三问Redis主从切换时锁丢了怎么办回答要点先承认问题的存在默认异步复制下主节点故障瞬间从节点没同步到锁数据锁会丢。然后说解决方案的取舍Redlock算法在网络分区和时钟偏差下也有争议更务实的做法是对强一致场景不上Redis锁改用ZK或Etcd或者用数据库唯一约束等机制做最终兜底。第四问ZooKeeper锁为什么不会出现锁丢失回答要点因为临时节点和会话强绑定。客户端会话未结束节点一定存在会话结束宕机、网络断开、心跳超时ZK自动删除节点。同时ZK的ZAB协议保证多数派同步主从切换不会丢数据。所以只要客户端节点还活着锁就在节点死了锁立刻释放不会出现死锁。第五问分布式锁的可重入是怎么实现的回答要点单机锁靠ThreadLocal计数分布式锁本质上一样在key的value里记录持有者标识同时在客户端本地维护一个ThreadLocal计数器。同一个线程再次加锁时如果持有者标识匹配且计数大于0则计数加1不触发真正的远端加锁释放时每释放一次计数减1减到0才真正删除远端key。7.2 实际项目中踩过的坑第一个坑Redis锁的value用了固定字符串导致误删锁。早期我在一个项目中直接用lock作为value某次一个节点GC停顿了十几秒锁过期后另一个节点拿锁第一个节点醒来后直接删掉了第二个节点的锁线上出现了一次重复扣款。修复方案很简单value改成UUID或实例ID线程ID删除前强制校验这个校验逻辑不能漏。第二个坑Redisson的tryLock(long waitTime, long leaseTime, TimeUnit unit)传了leaseTime但不知道看门狗会关闭。我当时把一个定时任务的锁设成30秒结果某次任务跑了40秒锁在第30秒就没了两个节点同时跑任务重复处理了一批数据。排查了很久才意识到传了leaseTime就失去自动续期。解决方案是改成只传waitTime让看门狗接管续期。第三个坑ZK锁的sessionTimeout设太小网络抖动导致锁自动释放。一次线上发布某节点频繁GCZK认为会话过期删掉了临时节点重构后的客户端又重新获取锁出现了本不该出现的双执行。后来把sessionTimeout从5秒调到20秒配合JVM的GC暂停时间一起评估再也没出现过这个问题。第四个坑数据库唯一索引锁在压力测试下出现大量死锁和超时。原因是多个线程以不同顺序获取多把锁造成循环等待。后来我对所有锁操作统一了获取顺序并把锁粒度进一步细化死锁问题才消失。这个问题提醒我数据库锁不只是建表插数据那么简单并发控制的基本功该有的还得有。分布式锁没有银弹。这四种方案里没有“最好”只有“最合适”。我见过Redis锁解决日订单千万级的并发问题也见过ZK锁保障资金链路的稳定。把每种方案的原理搞清楚把它们的薄弱点记在心里选型时自然会给出合理的判断。如果你也在做相关技术选型可以参考这个思路梳理一遍自己的业务约束比在网上搜“哪个方案最强”要靠谱得多。
返回列表