【Redis 进阶】分布式锁
一、线程安全与分布式锁的必要性
(一)线程安全问题
在同一个进程中,多个线程访问同一资源时,容易出现线程安全问题。为保证多个线程并发执行时逻辑顺序正确,需使用锁机制。
(二)分布式系统中的挑战
分布式系统中存在众多进程,传统的单机锁无法满足需求,因此引入分布式锁来解决跨进程资源访问的同步问题。
数据支撑:
- 根据《分布式系统原理与范型》一书,超过80%的分布式系统故障源于资源竞争问题。
- 据国际标准ISO/IEC 2382-15定义,分布式锁需满足互斥性、容错性和一致性。
二、分布式锁的基础实现
(一)买票服务案例
以买票服务为例,当票数为1时,若两个客户端同时尝试购票,将产生冲突。此时,分布式锁发挥作用。
分布式锁实质上是一个或多个独立的服务器程序,为其他服务器提供加锁服务。Redis是常见的分布式锁实现方案之一,此外,MySQL和Zookeeper等组件亦可达到类似效果。
(二)加锁与解锁机制
客户端购票时,在Redis中插入特殊键值对,操作完成后删除该键值对。其他服务器尝试购票时,先检查Redis中是否存在该键值对,若存在则加锁失败。可使用setnx
命令实现加锁,del
命令实现解锁。
注意事项:
setnx
命令的全称为“SET if Not eXists”,用于在键不存在时设置键值对。- 解锁操作需先判定当前服务器是否为加锁服务器,避免误解锁导致系统异常。
三、分布式锁的优化策略
(一)过期时间与动态续约
为防止服务器故障导致死锁,引入过期时间机制。同时,采用动态续约策略,在锁即将到期时自动续约。
过期时间的设置:
- 过期时间设置过短可能导致锁提前释放,影响业务逻辑的正确性。
- 过期时间设置过长则可能导致锁释放不及时,增加系统资源的占用。
动态续约机制:
- 初始设置锁的过期时间为1秒。
- 在锁剩余300毫秒时,自动续约1秒。
- 即使服务器崩溃,锁也会在很短时间内释放,避免资源长时间被占用。
技术实现:
- 动态续约需由独立的线程负责,定期检查锁的状态并进行续约操作。
- 续约操作通过Redis的
EXPIRE
命令实现,确保锁的过期时间动态调整。
(二)校验机制
为确保解锁操作的准确性,引入校验机制,包括服务器标号和身份验证。
服务器标号:
- 每个服务器分配唯一的身份标识符,用于区分不同的服务器实例。
- 在加锁时,将服务器标号作为键值对的一部分存储在Redis中。
身份验证:
- 解锁操作前,先查询Redis中存储的服务器标号。
- 判定当前服务器是否为加锁服务器,只有匹配的服务器才能执行解锁操作。
(三)原子操作
为避免多个线程同时解锁同一资源导致的重复删除问题,采用Lua脚本实现原子解锁操作。
Lua脚本的优势:
- Lua脚本在Redis中执行时是原子的,确保解锁操作的完整性和一致性。
- 脚本执行时间通常小于1毫秒,满足高并发场景下的性能需求。
技术实现:
- 编写Lua脚本,包含查询服务器标号和删除键值对的操作。
- 将脚本上传至客户端,由客户端控制Redis执行该脚本。
示例代码:
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
KEYS[1]
:锁的键名。ARGV[1]
:当前服务器的标号。