26人参与 • 2025-05-14 • Redis
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现。如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此之间的干扰。
实现分布式锁的方式有很多,可以通过各种中间件来进行分布式锁的设计,包括redis、zookeeper等。
如下图所示:
如下图所示锁的流程:
属于最简单的锁,不推荐生产使用。
setnx toilet_1 "occupied" # 尝试锁门
问题:如果客户端崩,永远被占着(死锁)。
属于对setnx命令的改进版:
setnx toilet_1 "occupied" expire toilet_1 30 # 30秒后自动解锁
仍然有问题:两条命令不是原子的,可能在setnx和expire之间崩溃。
该命令可使用于生产级方案:
set toilet_1 "user_123" nx ex 30 # 原子操作:锁门+设置30秒自动开锁
当需要更高可靠性时,redis作者antirez提出的分布式锁算法:
获取当前毫秒级时间戳t1
依次向n个独立的redis实例申请锁
计算获取锁总耗时 = 当前时间t2 - t1
锁实际有效时间 = 初始设置时间 - 获取锁总耗时。
代码示例:
// redissonredlock.trylock()的核心逻辑 while (waittimeremaining > 0) { long start = system.nanotime(); // 尝试从多数节点获取锁 int acquiredlocks = tryacquiremultiplelocks(); if (acquiredlocks >= majority) { // 计算实际有效时间 long elapsed = system.nanotime() - start; long locktime = leasetime - timeunit.nanoseconds.tomillis(elapsed); if (locktime > 0) { // 对所有成功节点设置统一过期时间 schedulelockexpiration(locktime); return true; } // 超时则释放已获得的锁 releaseacquiredlocks(); } // 等待随机时间后重试 waittimeremaining -= calculatewaittime(); }
1. 仍然存在的竞争问题
2. 网络分区问题
当发生网络分区时:
3. 性能开销
获取多个锁的延迟显著高于单节点:
而对于redlock的本质作用确实主要是为了解决单点故障问题,而不是提升并发性能,并未彻底解决一致性,如果要解决一致性问题,需要结合防护令牌或分布式事务。
当发生锁竞争的时候,假设5节点redlock:
此时:
代码示例:
// 获取锁时返回单调递增的token lockresult result = redlock.trylockwithtoken(); long token = result.gettoken(); // 操作资源时验证token if (resource.getcurrenttoken() < token) { resource.write(data, token); } else { throw new concurrentmodificationexception(); }
在上述的章节了解到,防护令牌可以解决锁竞争一致性的问题,那么如果在锁执行过程中,过期时间到期,而业务还没执行完,那么该怎么办呢?
看门狗(watchdog)机制是redis分布式锁中确保业务执行期间锁不会意外释放的关键设计,尤其在redisson等客户端中广泛使用。
当业务执行时间超过锁的初始过期时间时,防止其他客户端提前获取锁导致数据竞争。
流程:
// 获取锁(默认30秒看门狗时间) rlock lock = redisson.getlock("order_lock"); lock.lock(); // 内部启动看门狗线程 try { // 执行业务逻辑(可能超过30秒) processorder(); } finally { lock.unlock(); // 释放时会停止看门狗 }
锁获取时:
public void lock() { try { lockinterruptibly(); } catch (interruptedexception e) { thread.currentthread().interrupt(); } } public void lockinterruptibly() throws interruptedexception { // 尝试获取锁,默认leasetime=30秒 tryacquireasync(leasetime, timeunit.milliseconds).sync(); // 启动看门狗线程 scheduleexpirationrenewal(); }
看门狗线程:
protected void scheduleexpirationrenewal() { thread renewalthread = new thread(() -> { while (!thread.currentthread().isinterrupted()) { // 每10秒(leasetime/3)续期一次 try { thread.sleep(leasetime / 3); // 通过lua脚本续期 string script = "if redis.call('hexists', keys[1], argv[2]) == 1 then " + "return redis.call('pexpire', keys[1], argv[1]); " + "else return 0; end"; redis.eval(script, collections.singletonlist(getname()), internallockleasetime, getlockname(thread.currentthread().getid())); } catch (interruptedexception e) { thread.currentthread().interrupt(); } } }); renewalthread.start(); }
参数和配置方式如下:
jpg jpg
用一个电影院抢座的例子,通过java代码展示redis分布式锁的实际应用。这个场景非常贴近生活,容易理解分布式锁的必要性。
假设有一个热门电影场次,多个用户同时在线选座,我们需要保证:
1、基础配置
首先添加redis和redisson(redis java客户端)依赖:
<!-- pom.xml --> <dependency> <groupid>org.redisson</groupid> <artifactid>redisson</artifactid> <version>3.16.8</version> </dependency>
初始化redis连接:
public class redislockdemo { private static redissonclient redisson; static { config config = new config(); config.usesingleserver().setaddress("redis://127.0.0.1:6379"); redisson = redisson.create(config); } }
简单实现:选座锁
1. 获取座位锁
public boolean lockseat(string seatnumber, string userid) { // 获取分布式锁对象 rlock lock = redisson.getlock("seat:" + seatnumber); try { // 尝试加锁,waittime=0表示不等待,leasetime=10分钟自动解锁 return lock.trylock(0, 10, timeunit.minutes); } catch (interruptedexception e) { thread.currentthread().interrupt(); return false; } }
2. 释放座位锁
public void unlockseat(string seatnumber, string userid) { rlock lock = redisson.getlock("seat:" + seatnumber); // 检查是否还被当前线程持有 if (lock.isheldbycurrentthread()) { lock.unlock(); } }
3. 完整选座流程
public boolean selectseat(string seatnumber, string userid) { if (!lockseat(seatnumber, userid)) { system.out.println(userid + " 抢座失败,座位已被锁定"); return false; } try { system.out.println(userid + " 成功锁定座位 " + seatnumber); // 模拟用户支付流程 boolean paid = mockpaymentprocess(userid); if (paid) { system.out.println(userid + " 支付成功,座位确认"); return true; } else { system.out.println(userid + " 支付超时,座位释放"); return false; } } finally { unlockseat(seatnumber, userid); } } private boolean mockpaymentprocess(string userid) { // 模拟50%概率支付成功 try { thread.sleep(2000); // 模拟支付思考时间 } catch (interruptedexception e) { thread.currentthread().interrupt(); } return new random().nextboolean(); }
3、高级特性:锁续期
当用户支付时间可能超过10分钟时,需要自动续期:
public boolean lockseatwithrenewal(string seatnumber, string userid) { rlock lock = redisson.getlock("seat:" + seatnumber); try { // 获取锁,并设置看门狗自动续期(默认30秒) lock.lock(); // 启动一个线程定期续期 new thread(() -> { while (!thread.currentthread().isinterrupted()) { try { thread.sleep(5000); // 每5秒续期一次 lock.expire(10, timeunit.minutes); // 续期10分钟 } catch (interruptedexception e) { thread.currentthread().interrupt(); } } }).start(); return true; } catch (exception e) { return false; } }
4、测试用例
public static void main(string[] args) { redislockdemo demo = new redislockdemo(); // 模拟3个用户同时抢5号座位 new thread(() -> demo.selectseat("a05", "用户1")).start(); new thread(() -> demo.selectseat("a05", "用户2")).start(); new thread(() -> demo.selectseat("a05", "用户3")).start(); }
输出可能结果:
用户1 成功锁定座位 a05用户2 抢座失败,座位已被锁定用户3 抢座失败,座位已被锁定用户1 支付成功,座位确认
5、关键点解析
seat:a05
明确表示对a05座位的锁6、对比生活场景
技术概念 | 电影院例子 |
---|---|
redis服务器 | 电影院售票系统 |
分布式锁 | 座位锁定状态 |
锁的key | 座位号(如a05) |
锁的value | 售票员记录的本子(谁锁的) |
锁过期时间 | "保留座位10分钟"的告示牌 |
获取锁失败 | 看到座位已经被标记"已预订" |
锁续期 | 顾客请求延长保留时间 |
这个例子展示了:
通过电影院选座这种熟悉的场景,应该能更直观地理解redis分布式锁的工作机制了。实际开发中,使用redisson等成熟客户端可以避免很多边界条件的处理。
时钟漂移问题:
持久化延迟:
长时间阻塞:
redis分布式锁凭借其优异的性能和足够的可靠性,已成为互联网公司的首选方案。理解其实现原理和限制条件,能够帮助我们在不同业务场景中做出合理的技术选型。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论