如何使用 Redis 实现分布式锁
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我是个笨蛋
嗯,只要我们在本地系统里工作,一切都顺畅无比。所以我们才说“没有比127.0.0.1更好的地方了”,但醒醒吧,面对现实吧!
然而,在生产环境中,事情并非总能如预期般顺利进行,尤其是在运行应用程序的多个实例时。
🚀 正如你所看到的,如果我们的应用程序有多个实例正在运行,假设我们的客户发出请求,将数据库中的某个用户标记为付费用户。
- 客户端将向我们的服务器发出请求
- 请求将到达我们的负载均衡器。
- 其中一个实例会收到请求,并向我们的数据库执行写入查询。
看起来好像没问题吧?目前为止都没出过问题。
嗯,目前为止没有问题。但如果我们想编写类似这样的业务逻辑呢?
- 从数据库中获取用户
- 检查用户是免费用户还是已付费用户。
- 如果免费,则将其标记为付费并保存到数据库中。
- 如果已付款,请在回复中发送“已付款”。
⚡️ 我们知道(假设这里使用的是 MySQL),MySQL 数据库符合 ACID 原则,这意味着任何查询都是原子性的且相互隔离的。也就是说,MySQL 查询会以原子方式执行,要么成功,要么失败,不会中途终止。
🤔 但这里有个问题。想想,想想……
- 步骤 1:我们正在获取用户(原子事务)
- 步骤二:在代码中运行一些业务逻辑
- 步骤 3:如果用户未付款,则更新 MySQL 记录(原子事务)
如果在步骤 2 中,又有一个取消付款的请求,那么该查询会先运行并将用户标记为免费,然后步骤 3 运行并将用户标记为已付费,会发生什么情况?
🕺🏻 太棒了!用户无需付费即可使用我们的产品。
锁定
🔐 锁是一种结构,它一次只允许一个线程进入临界区(不应该被多个工作线程访问的代码块)。
因此,我们将在操作完成前获取锁,并在操作完成后释放锁:
- 步骤 0:尝试获取锁
- 步骤 1:如果已获取,我们将获取用户(原子事务)
- 步骤二:在代码中运行一些业务逻辑
- 步骤 3:如果用户未付款,则更新 MySQL 记录(原子事务)
- 步骤 4:释放锁
😅 问题
现在问题来了,如果我们使用内存锁数据结构或任何基于内存的锁,它只能用于我们应用程序的一个实例。那么运行相同代码并更新数据库的其他实例怎么办?
这就引出了分布式锁的概念。
🔓 分布式锁定
在这里,锁充当集中式服务,如果我们的服务的一个实例获得了锁,那么其他实例就不能使用同一个密钥获得锁。
支付服务中可能存在什么关键因素?
🔓 对于进行支付的用户,支付密钥可能是“PAYMENT_” + user_id + amount 的组合。
每个用户都将拥有唯一的密钥。无论用户进行支付还是取消支付,此密钥都保持不变。因此,当用户进行支付或取消支付时,其他操作将无法进行,因为这两个操作都会尝试使用同一个密钥。
💭 Key、Acquire lock、release lock 到底是什么?最重要的是,Redis 是如何被使用的?
🎈 使用 Redis 实现分布式锁
使用单个 Redis 实例:
但使用单个 Redis 实例存在以下几个问题:
- 单个实例可能发生故障,且获取的锁可能不会被释放。
- 如果使用两个实例(主从模式),则一个客户端将获取其中一个实例的锁。
- 主节点必须与副本节点进行通信才能实现同步。这种通信本身就是一种异步通信。
🚀 因此,如果主节点获取了锁,但在与副本节点通信期间,如果主节点在与副本节点同步之前宕机,则副本节点将成为新的主节点,并可以获取之前在主节点上获取的同一键的锁。
即使有两个实例(主副本),我们的服务的两个实例也能够获取 Redis 上的锁。
使用红锁算法:
获取锁:我们将尝试在多个 Redis 实例上获取锁,并设置锁的过期时间。
锁验证:如果主要 Redis 实例都为客户端获取了锁,则认为该锁已成功获取。
释放锁:释放锁时,所有实例都会释放该锁。
是的,就是这样。
❤️感谢阅读,欢迎订阅我们的新闻简报,获取更多类似文章:https ://www.serversidedigest.com/
更多信息:-
- Java 版 Jedis:- https://redis.io/docs/latest/develop/connect/clients/java/jedis/
- Golang 中的 Redis 客户端:- https://github.com/redis/go-redis





