分布式锁
什么是分布式锁
在分布式系统中,确保多个进程或线程之间互斥访问共享资源的机制。在多台服务器或多进程环境下,通常会遇到多个实例对同一个资源进行操作的竞争问题,分布式锁可以保证同一时刻只有一个实例能够操作资源,避免数据不一致或竞争条件。
分布式锁的使用场景
- 库存扣减:当多个用户同时抢购商品时,确保同一时刻只有一个进程能成功扣减库存。
- 秒杀、抢购活动:在高并发场景下,保证只有一个用户可以抢到某个限量商品。
- 定时任务调度:当多个实例运行定时任务时,通过分布式锁来确保任务不会被重复执行。
- 分布式事务:当多个服务对同一资源进行操作时,利用分布式锁来保证事务的一致性。
- 防重提交:例如表单提交等操作,需要通过分布式锁来防止用户重复提交。
分布式锁的实现方式
基于数据库的分布式锁
通过数据库表中的行记录来实现锁,常用的方式是利用数据库的INSERT或UPDATE操作。
- 优点:实现简单,无需引入外部组件。
- 缺点:数据库性能可能成为瓶颈。
基于数据库实现分布式锁的常见方式是利用数据库中的行记录作为锁标识。通过事务和SQL操作(如INSERT或UPDATE)来获取或释放锁。
实现步骤
一:创建数据库表作为锁资源
1 | CREATE TABLE `distributed_lock` ( |
二:获取锁
1 | INSERT INTO distributed_lock (lock_name, lock_owner, lock_time) |
如果插入失败(即锁已经存在),则说明锁已经被其他线程持有。
三:释放锁
1 | DELETE FROM distributed_lock |
使用lock_name来确保只有锁的持有者能够释放锁。
四:超时机制
为了防止死锁,可以引入超时机制,如记录锁的创建时间,并在锁超时后允许其他线程获取锁。可以通过定期检查锁的时间字段并自动删除过期的锁来实现。
1
2 DELETE FROM distributed_lock
WHERE lock_name = 'lock_name' AND lock_time < (CURRENT_TIMESTAMP - INTERVAL '30' SECOND);
基于Redis的分布式锁
使用缓存工具(如Redis)的SETNX命令(Set if Not Exists)和过期时间TTL来实现。
- 优点:Redis性能高,延迟低,支持TTL防止死锁。
- 缺点:需要考虑Redis主从复制时的数据一致性问题。
实现步骤
一:获取锁
通过SETNX(set if not exixts)命令来实现获取锁的逻辑,如果键不存在(即锁未被占用)SETNX会设置键并返回成功。否则返回失败,表示锁已经被其他线程占用。
1 | set lock_key unique_value NX PX 10000 |
- lock_key 锁的名字,表示这把锁的唯一标识
- unique_key 唯一标识,确保客户端释放锁时不会误删别的客户端的锁
- NX 表示只有当键不存在时才进行设置,防止锁被其他客户端持有
- PX 10000:设置锁的自动过期时间(单位为毫秒)
1 |
二:设置锁的过期时间
通过EXPIRE或在SET命令中直接指定过期时间来避免死锁。当锁的持有者出现故障或没有正常释放锁时,锁会自动过期,保证不会长时间占用。
三: 释放锁
锁的持有者完成任务后,需要主动释放锁。这个操作通过DEL命令来删除锁对应的Redis键。
只有当当前客户端仍然持有锁时才能释放锁。可以通过Lua脚本确保删除锁的操作是原子性的(即读取锁的值和删除锁必须在同一个原子操作中完成)。
1 | if redis.call("GET", KEYS[1]) == ARGV[1] then |
- KEYS[1]:锁的键名
- ARGV[1]:客户端的唯一标识
- 如果当前客户端持有的锁和Redis中的锁值一样,则删除该锁
四:防止误删锁
在分布式环境中,可能会出现锁被误删的情况(比如:持有锁的客户端任务超时而锁过期,之后某个新任务获得了锁,但之前的任务仍然认为自己持有锁并执行了DEL)。为防止这种情况,需要确保只有当前持有锁的客户端才能释放锁,可以通过对锁的唯一标识进行验证。
五:自动续期
如果业务执行时间可能会超过锁的过期时间,可以实现自动续期机制。在持有锁的客户端每隔一段时间(例如一半的过期时间)检查锁是否仍然持有,如果是。则延长锁的过期时间
基于Redission的分布式锁
基于zookeeper的分布式锁
Zookeeper是一个分布式协调服务,利用其顺序临时节点特性,可以实现分布式锁。
- Zookeeper具有强一致性,可靠性高,适用于分布式事务。
- 缺点:实现相对复杂,性能相对Redis较低。