悲观锁,乐观锁和redis分布式锁
悲观锁(Pessimistic Lock)
为什么叫 "悲观"?
因为它 "悲观" 地认为并发操作一定会发生冲突,所以在操作数据之前,先加锁,确保其他事务无法修改这条数据,直到当前事务完成。
实现方式(数据库层面):
-
SELECT ... FOR UPDATE(MySQL) -
SELECT ... WITH (UPDLOCK)(SQL Server) -
其他数据库的排他锁机制
特点:
-
先锁再操作,防止并发修改
-
适用于高并发写操作(如抢购、库存扣减)
-
可能降低并发性能(锁会阻塞其他事务)
示例(ThinkPHP):
乐观锁(Optimistic Lock)
为什么叫 "乐观"?
因为它 "乐观" 地认为并发操作不会冲突,所以 不加锁,而是在更新时检查数据是否被修改过(通常用版本号或时间戳)。
实现方式:
-
版本号机制(
version字段) -
时间戳机制(
update_time字段) -
CAS(Compare-And-Swap)(如 Redis)
特点:
-
不加锁,先操作再检查冲突
-
适用于读多写少的场景
-
冲突时需要重试或回滚
示例(ThinkPHP):
对比总结
|
特性 |
悲观锁(Pessimistic Lock) |
乐观锁(Optimistic Lock) |
|---|---|---|
|
加锁方式 |
先加锁再操作( |
不加锁,更新时检查冲突 |
|
适用场景 |
高并发写操作(如抢购) |
读多写少(如文章编辑) |
|
性能影响 |
可能降低并发性能(锁阻塞) |
无锁,冲突时才处理 |
|
实现方式 |
数据库锁机制 |
版本号、时间戳、CAS |
|
冲突处理 |
其他事务会被阻塞 |
需要重试或回滚 |
如何选择?
-
悲观锁:数据竞争激烈,必须保证数据一致性(如支付、库存扣减)。
-
乐观锁:冲突概率低,希望提高并发性能(如文章编辑、评论更新)。
redis分布式锁:
这段代码的作用是 使用 Redis 的 SETNX(SET if Not eXists)实现分布式锁,防止并发重复提交(如重复提现)。具体解析如下:
代码解析
-
setLockNx($withdrawLockKey, 30)-
调用一个自定义函数(可能是封装了 Redis 的
SETNX命令)。 -
$withdrawLockKey:锁的唯一标识(如用户ID + 业务类型,例如withdraw:user123)。 -
30:锁的自动过期时间(单位:秒),防止锁未释放导致死锁。
-
-
!setLockNx(...)-
如果获取锁失败(返回
false),说明锁已存在(即当前有其他请求正在处理相同业务)。 -
直接返回错误提示,阻止重复操作。
-
-
json_exit_Base64(401, ...)-
返回 HTTP 401 状态码和 Base64 编码的 JSON 错误消息(可能是项目约定的通信格式)。
-
提示用户:"处理中,请勿重复点击"。
-
-
注释说明
-
//提现锁,处理完手动解锁:提示开发者需要在业务逻辑完成后 手动释放锁(如调用delLock($withdrawLockKey)),否则锁会在 30 秒后自动过期。
-
实现原理(Redis 分布式锁)
-
加锁
通过 Redis 的
SETNX命令实现原子性锁:(注:高版本 Redis 支持
SET $withdrawLockKey 1 NX EX 30一步完成) -
解锁
业务处理完成后需手动删除 key:
适用场景
-
提现/支付防重:防止用户多次点击导致重复扣款。
-
秒杀/库存扣减:避免超卖问题。
-
任何需要分布式环境下的互斥操作(如定时任务防并发执行)。
注意事项
-
锁的粒度
-
锁的 key 需要足够具体(例如包含用户ID),避免不同用户互相阻塞。
-
-
锁的过期时间
-
过期时间(如30秒)需大于业务处理时间,但不宜过长(否则阻塞其他请求)。
-
-
原子性问题
-
确保
SETNX + EXPIRE是原子操作(如用 Redis 2.6.12+ 的SET命令带NX和EX参数)。
-
-
手动解锁
-
如果业务逻辑异常退出,需在
finally块中释放锁,或依赖自动过期。-
- 安全释放锁:
-
解释
releaseLockNx($withdrawLockKey)的含义这段代码的作用是 安全释放 Redis 分布式锁,而不仅仅是简单执行
DEL命令。它通常是一个 自定义函数,封装了更严谨的解锁逻辑,确保 只有锁的持有者才能释放锁,避免误删其他客户端的锁。
为什么不是
delLock($withdrawLockKey)?-
直接
DEL的问题如果简单地用
DEL删除锁:-
风险:假设客户端 A 获取锁后因某些原因(如网络延迟、GC 停顿)导致锁过期,此时客户端 B 获取了锁。如果 A 仍然调用
DEL,会错误地删除 B 的锁,导致数据不一致。
-
-
安全释放锁的方案
正确的做法是 在释放锁时验证持有者身份,通常通过以下方式实现:
-
方案 1:Lua 脚本(原子性操作)
检查锁的值是否匹配当前客户端的唯一标识(如 UUID),匹配时才删除:
-
方案 2:Redis 事务
使用
WATCH+GET+DEL组合(但不如 Lua 脚本简洁)。
-
releaseLockNx($withdrawLockKey)的典型实现该函数内部可能包含类似以下逻辑(伪代码):
与
delLock($withdrawLockKey)的区别函数名
行为
安全性
delLock($key)直接删除 key,不检查持有者
低(可能误删其他客户端锁)
releaseLockNx($key)验证持有者身份后删除 key
高(避免误删)
为什么项目中选择
releaseLockNx而非delLock?-
防御性编程
防止因客户端超时、重试等异常场景导致锁被错误释放。
-
代码可读性
releaseLockNx明确表达了“安全释放锁”的意图,而delLock可能被误解为简单删除。
-
-
-

浙公网安备 33010602011771号