一、Redis 事务的基本概念
1.1 什么是 Redis 事务
Redis 事务是一组命令的集合,它允许将多个命令打包,然后一次性、按顺序地执行。在事务执行期间,不会被其他客户端发送的命令打断,保证了事务内命令的原子性(这里的原子性与传统关系型数据库有所区别,后续会详细说明)。简单来说,Redis 事务就是 "要么全部执行,要么全部不执行"(在不考虑中途出错的特殊情况时)。
Redis 事务通过以下三个核心命令实现:
MULTI:标记事务开始EXEC:执行事务中的所有命令DISCARD:取消事务,放弃执行事务块内的所有命令
实际应用场景示例
-
电商系统:
- 用户下单时需要扣减商品库存、创建订单记录
- 这两个操作可以放在一个 Redis 事务中执行
- 确保这两个操作要么都成功,要么都失败
- 避免出现库存扣减了但订单没创建,或者订单创建了但库存没扣减的情况
-
社交网络:
- 用户关注某人时需要更新双方的关注/粉丝列表
- 这两个更新操作可以放在同一事务中
-
游戏系统:
- 玩家交易物品时需要同时减少卖方物品和增加买方物品
- 这两个操作可以原子性地执行
1.2 Redis 事务与传统数据库事务的区别
传统关系型数据库(如 MySQL)的事务遵循 ACID 原则(原子性、一致性、隔离性、持久性),而 Redis 事务在特性上与传统数据库事务存在显著差异:
| 特性 | 传统关系型数据库事务 | Redis 事务 |
|---|---|---|
| 原子性 | 严格支持,事务内操作要么全成功,要么全回滚 | 部分支持,事务内命令执行中出错时:<br>- 已执行命令无法回滚<br>- 未执行命令不再执行 |
| 一致性 | 严格保证,事务执行前后数据满足完整性约束 | 基本保证,需要依赖开发者:<br>- 合理设计命令序列<br>- 正确处理异常情况 |
| 隔离性 | 支持多种隔离级别(读未提交、读已提交、可重复读、串行化) | 事务执行期间不会被其他命令打断,类似串行化隔离级别 |
| 持久性 | 支持,通过事务日志等机制保证数据持久化 | 依赖 Redis 的持久化策略(RDB、AOF),事务本身不直接保证持久性 |
关键差异分析
-
原子性实现方式:
- MySQL:通过回滚日志(undo log)实现回滚
- Redis:没有回滚机制,错误发生后继续执行后续命令
-
隔离级别:
- MySQL:提供多种隔离级别选择
- Redis:只有一种类似串行化的隔离级别
-
错误处理:
- MySQL:语法错误会导致整个事务回滚
- Redis:语法错误会导致整个事务不执行,运行时错误不影响已执行命令
二、Redis 事务的相关命令
Redis 提供了一组专门用于操作事务的命令,这些命令构成了 Redis 事务功能的核心。掌握这些命令的用法对于在分布式环境中确保数据操作的原子性至关重要。下面详细介绍每个命令的功能、语法、使用场景及实际应用示例。
2.1 MULTI:开启事务
MULTI命令用于标记一个事务的开始,执行该命令后,Redis 会进入事务模式。在此模式下,客户端发送的所有命令都会被放入事务队列中暂存,而不是立即执行。
语法
MULTI
使用示例
127..0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET user:1001:name "张三"
QUEUED
127.0.0.1:6379(TX)> INCR user:1001:visits
QUEUED
详细说明
- 执行
MULTI命令后,Redis 会返回OK响应,表示已成功进入事务模式 - 此时客户端提示符会从
127.0.0.1:6379>变为127.0.0.1:6379(TX)>,表示当前处于事务上下文 - 后续所有非事务控制命令(如
SET、GET等)都会被服务器返回QUEUED响应,表示命令已加入事务队列 - 事务队列中的命令会按照先进先出(FIFO)的顺序保存
2.2 EXEC:执行事务
EXEC命令用于触发事务的执行,Redis 会按顺序执行事务队列中的所有命令。该命令是确保事务原子性的关键操作。
语法
EXEC
使用示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET cart:1001:item1 "手机"
QUEUED
127.0.0.1:6379(TX)> SET cart:1001:item2 "耳机"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
详细说明
- 执行
EXEC后,Redis 会依次执行队列中的所有命令 - 返回结果是一个数组,其中每个元素对应事务中一个命令的执行结果
- 如果事务中某个命令执行失败(如语法错误),不会影响其他命令的执行
- 事务执行后会自动取消所有
WATCH的键 - 典型应用场景:电商购物车结算时,需要同时更新库存、订单和支付信息
2.3 DISCARD:取消事务
DISCARD命令用于显式地放弃当前事务,清空事务队列并退出事务状态。
语法
DISCARD
使用示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET temp:data "test"
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> GET temp:data
(nil)
详细说明
- 执行
DISCARD后,事务队列中的所有命令都会被丢弃 - 客户端会立即退出事务状态
- 所有被
WATCH的键也会被自动取消监视 - 适用场景:当检测到业务条件不满足时,可以安全地取消事务而不会产生副作用
2.4 WATCH:监视键
WATCH命令实现了Redis的乐观锁机制,用于在事务执行前监控指定的键,如果这些键被其他客户端修改,则当前客户端的事务会被拒绝执行。
语法
WATCH key [key ...]
使用示例
情况一:监视的键未被修改
# 客户端A
127.0.0.1:6379> WATCH account:1001:balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY account:1001:balance 50
QUEUED
127.0.0.1:6379(TX)> INCRBY account:1002:balance 50
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 450
2) (integer) 550
情况二:监视的键被其他客户端修改
# 客户端A
127.0.0.1:6379> WATCH inventory:item001
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECR inventory:item001
QUEUED
# 客户端B在其他连接中执行
127.0.0.1:6379> DECR inventory:item001
(integer) 99
# 客户端A
127.0.0.1:6379(TX)> EXEC
(nil)
详细说明
WATCH可以监视一个或多个键- 监视会持续到
EXEC命令执行(事务成功或失败) - 如果被监视的键在
WATCH之后、EXEC之前被修改,整个事务会被取消 - 典型应用场景:账户转账、库存扣减等需要保证数据一致性的操作
- 监视机制是Redis实现CAS(Check-And-Set)操作的基础
2.5 UNWATCH:取消监视
UNWATCH命令用于显式取消对所有键的监视,通常在确定不需要继续监视或者需要更改监视策略时使用。
语法
UNWATCH
使用示例
127.0.0.1:6379> WATCH order:status user:1001:credits
OK
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET test "value"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
详细说明
- 执行
UNWATCH后,所有被WATCH的键都会被取消监视 - 事务执行成功(
EXEC)或取消(DISCARD)时也会自动执行UNWATCH - 适用场景:当业务逻辑发生变化,不再需要之前监视的键时
- 注意:
UNWOSTCH不能只取消对特定键的监视,它会取消所有键的监视
事务命令综合使用建议
- 将
WATCH、MULTI/EXEC组合使用可以实现可靠的乐观锁 - 事务中的命令不宜过多,建议控制在合理范围内以避免长时间阻塞
- Redis事务不支持回滚,需要开发者自行处理部分失败的情况
- 在集群模式下,所有事务操作必须落在同一个节点上(使用相同的hash slot)
三、Redis 事务的执行流程详解
3.1 开启事务(MULTI)
客户端通过发送MULTI命令来显式地开启一个Redis事务。当Redis服务器接收到MULTI命令后,会进行以下操作:
- 将客户端的状态标志
flags中的REDIS_MULTI位设置为1,表示该客户端已进入事务状态 - 初始化一个空的事务命令队列(FIFO队列),用于存储后续接收到的命令
- 返回"OK"响应给客户端,表示事务已成功开启
例如,在Redis-cli中执行:
127.0.0.1:6379> MULTI
OK
3.2 添加命令到事务队列
在事务开启后(MULTI之后,EXEC之前),客户端发送的所有常规Redis命令都不会立即执行,而是被加入事务队列。具体流程如下:
-
命令入队检查:
- 服务器首先检查命令语法是否正确(参数数量、参数类型等)
- 如果语法错误(如命令不存在或参数错误),立即返回错误信息,如
(error) ERR unknown command 'SETX' - 如果语法正确,将命令封装为事务队列节点并加入队列
-
入队响应:
- 对于成功入队的命令,统一返回"QUEUED"响应
- 例如:
127.0.0.1:6379> SET key1 value1 QUEUED 127.0.0.1:6379> INCR key2 QUEUED
-
注意事项:
- 不会检查命令的逻辑可行性(如对字符串执行INCR)
- 不支持事务中的事务(嵌套MULTI会被拒绝)
- 某些命令(如INFO、SHUTDOWN等)会被立即执行而不会入队
3.3 执行或取消事务
3.3.1 执行事务(EXEC)
当客户端发送EXEC命令时,Redis会:
-
前置检查:
- 验证客户端是否处于事务状态
- 检查事务队列是否为空(空队列执行会返回错误)
-
执行阶段:
- 按FIFO顺序依次执行队列中的所有命令
- 每个命令执行时:
- 检查键类型是否正确(如对字符串执行LPUSH会报错)
- 执行过程中某个命令失败不会影响其他命令的执行
- 不会自动回滚已执行的命令
-
结果返回:
- 将所有命令的执行结果按顺序放入数组返回
- 例如:
1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
-
状态清理:
- 清除客户端的
REDIS_MULTI标志 - 清空事务队列
- 取消所有
WATCH的键
- 清除客户端的
3.3.2 取消事务(DISCARD)
当客户端发送DISCARD命令时:
- 立即清空事务队列中的所有命令
- 将客户端状态从事务状态恢复为普通状态
- 取消所有被
WATCH的键 - 返回"OK"表示事务已取消
典型应用场景:当客户端发现事务中的命令可能有问题时,可以主动放弃当前事务。
3.4 带有WATCH的乐观锁事务
3.4.1 WATCH机制详解
WATCH命令为Redis事务提供了CAS(Check-And-Set)功能:
-
工作原理:
- 客户端可以
WATCH任意数量的键 - Redis会记录这些键的版本号(通过数据库的
dirty计数器实现) - 如果在
WATCH和EXEC之间这些键被修改过(包括过期),事务将失败
- 客户端可以
-
使用示例:
127.0.0.1:6379> WATCH balance OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> INCRBY balance 100 QUEUED 127.0.0.1:6379> EXEC (nil) # 如果balance在WATCH后被其他客户端修改过
3.4.2 执行流程差异
相比普通事务,带WATCH的事务在EXEC时会额外执行:
-
版本检查:
- 比较被
WATCH键的当前版本号与WATCH时的版本号 - 如果任何键的版本号不匹配,说明键已被修改
- 比较被
-
处理结果:
- 如果有键被修改:返回nil表示事务失败,不执行任何命令
- 如果所有键未修改:正常执行事务队列中的所有命令
-
注意事项:
WATCH的效果只对下一次EXEC有效UNWATCH可以手动取消所有WATCH的键WATCH的键即使被其他命令修改(如EXPIRE)也会导致事务失败
四、Redis 事务的特性深入分析
4.1 atomicity(原子性)
原子性是事务的核心特性之一,传统数据库事务的原子性是指事务内的操作要么全部执行成功,要么全部执行失败odel不会出现部分执行的情况。而 Redis 事务的原子性与传统数据库有所不同,需要分情况讨论:
情况 1:事务队列中存在语法错误的命令
详细说明: 当使用MULTI开启事务后,如果在命令peer-queueing阶段(即命令入队时)就出现语法错误,Redis会立即拒绝饶命令,moslem整个事务都不会被执行。这种在命令入队阶段就能检测到的错误称为"编译"错误。
典型场景:
- 命令参数个数错误
- 命令拼写错误
- 使用了不存在的命令
示例详解:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> readd key1Platform value7 # 拼写错误
(error) ERR unknown command 'readd'
127.0.0.1:6379(TX)> SET key8 # 缺少value参数
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
影响范围:
- 整个事务会被完全放弃
- 不会修改任何数据
- 客户端需要重新发起isco整个事务
情况 2:事务队列中存在逻辑问题
详细说明: 当命令语法正确但存在运行时错误时sun, Redis会执行所有正确的命令,仅对错误的命令返回错误信息。这种在读red执行阶段才能发现的错误称为"错误。
典型场景:
- 对字符串类型执行IN模tonumber操作
- 对不存在的键执行类操作
- 类型不匹配的操作
示例深入分析:
127.0.0.1:6379> SET counter "100"
OK
127.0.0.1:6379> MULTI
OK
127仴.0 backend.1:637(TX)> INCR counter # 正常执行
QUEUED
127.0.0.1:6379(TX)> LPUSH counter 200 # 类型错误
QUEUED
127.0.0.1:6379(TX)> SET status "done"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 101 # 成功
2) (error) WRONGTYPE Operation against a key having the wrong kind of value # 错误
3) OK # 成功
127.0.0.1:6379> GET counter
"101" # 前一命令已执行
127.0.0.1:6379> GET status
"done" # 后一命令也执行了
影响分析::
- 事务中部分命令成功执行
- 会修改部分数据
- 需要在客户端处理部分成功的情况
情况 3:客户端连接中断
详细说明: 如果在MULTI后但EXEC前连接断开,Redis会自动丢弃该客户端的事务队列。
恢复机制:
- 客户端需要重建连接
- 需要重新发起整个事务
- 可以使用WATCH命令防止数据变更
情况 4:服务器崩溃
持久化策略分析:
1.无持久化:
- anecdotal数据完全丢失
- 事务相当于从未执行
2.RDB持久化:
- 取决于最近一次快照时间 Objetivo- 可能丢失最近的事务(若未触发快照)
- 已快照过程中崩溃可能导致文件损坏
3.AOF模式:
Aappendakisync always情况下:- 每个命令都会写入磁盘
- 崩溃后Officer事务可能部分执行
appendfsync everysec情况下:- 可能丢失最后1秒的数据
appendfsync no情况下:- 依赖操作系统刷盘时机
最佳实践建议:
1.对于关键数据,建议:
- 使用WATCH命令实现乐观锁
- 在应用层实现补偿机制
- 必要时结合Lua脚本保证原子性
2.持久化配置建议 Innocent:
- 生产环境推荐RDB+AOF混合使用
- AOF建议配置 clustersappendfsync everysec
- 对于极高一致性要求的场景,可考虑always模式
3.监控与恢复: 下载
- 定期检查Redis日志
- 实现监控告警机制
- 准备数据恢复预案
总结: Redis事务的原子性特性需要根据具体场景来评估,在设计系统时应该充分考虑这种部分原子性特征,并采取相应的补偿措施来保证数据一致性。
4.2 一致性
一致性是指事务执行前后,数据库从一个合法状态转变为另一个合法状态,保证数据的完整性约束没有被破坏。在 Redis 中,事务的一致性保障机制相对简单,主要依赖于开发者合理设计命令和处理异常情况。根据事务执行过程中是否出现错误,Redis 事务的一致性可以分为以下几种典型场景:
情况 1:事务执行前没有错误(命令语法正确、无逻辑错误)
在这种理想情况下,所有命令都符合 Redis 语法规范且逻辑正确,事务执行后数据将保持完全一致。具体表现为:
- 事务队列中的所有命令都会按FIFO(先进先出)顺序执行
- 执行过程中不会出现命令失败的情况
- 数据从一个合法状态转变为另一个合法状态
典型应用场景:银行转账业务
MULTI
DECRBY account:A 100
INCRBY account:B 100
EXEC
在这个例子中,如果账户A有足够余额且两个账户都存在,执行后账户A减少100元,账户B增加100元,总金额保持不变,满足业务约束条件。
情况 2:事务执行前存在语法错误
Redis 采用"预检查"机制来保证一致性:
- 在命令入队时就会进行语法检查
- 如果发现语法错误(如命令不存在、参数数量错误等),整个事务会被标记为失败
- 执行EXEC时将拒绝执行整个事务队列
错误示例:
MULTI
SET key1 value1
SETT key2 value2 # 错误命令
SET key3 value3
EXEC
此时由于SETT命令语法错误,所有命令都不会执行,数据库保持原状。
情况 3:事务执行前存在逻辑错误
这类错误更隐蔽且需要开发者特别注意:
- 命令语法正确但执行时可能失败(如对字符串执行INCR操作)
- Redis不会自动回滚已执行的命令
- 需要开发者自行实现补偿机制
典型场景:库存管理
MULTI
DECR stock # 扣减库存
INCR orders # 增加订单
EXEC
如果orders键是字符串类型,INCR操作会失败,但库存已被扣减。此时应该:
# 检查执行结果
results = EXEC
if results[1] is error:
INCR stock # 手动回滚
情况 4:执行EXEC时服务器崩溃
这种情况下的一致性取决于持久化配置:
-
无持久化:
- 数据完全丢失
- 系统重启后处于初始空状态
- 从业务角度看可能违反一致性约束
-
RDB持久化:
- 崩溃时未触发RDB快照:等同无持久化
- 崩溃时正在写RDB:可能导致损坏的快照文件
- 能恢复到最后一次成功快照的状态
-
AOF持久化:
- appendfsync always:基本能保证事务持久性
- appendfsync everysec:可能丢失最近1秒的数据
- AOF重写时崩溃:需使用redis-check-aof工具修复
建议解决方案:
- 关键业务建议使用AOF+everysec配置
- 结合WAL(Write-Ahead Logging)模式
- 定期备份RDB文件到异地
4.3 隔离性
Redis通过单线程模型和特殊的事务机制提供强隔离性保障:
事务执行原子性
-
命令队列特性:
- MULTI开始事务后,所有命令进入队列
- EXEC时队列命令作为整体执行
- 执行期间不会被其他客户端命令打断
-
网络层隔离:
- 事务执行期间独占网络I/O
- 其他客户端命令进入等待队列
- 类似传统数据库的表级锁
示例时序:
Client1: MULTI
Client1: SET x 1
Client2: SET x 2 # 被阻塞
Client1: EXEC # 先执行
Client2: SET x 2 # 后执行
WATCH机制
-
乐观锁实现:
WATCH key val = GET key MULTI SET key newVal EXEC # 如果key被修改则失败 -
监控原理:
- 维护被监视键的版本号
- EXEC时检查版本是否变化
- 变化则放弃整个事务
-
典型应用:
- 秒杀系统库存扣减
- 账户余额检查
- 分布式锁实现
隔离级别对比
| 特性 | Redis事务 | SQL Serializable |
|---|---|---|
| 脏读 | 不可能 | 不可能 |
| 不可重复读 | 可能* | 不可能 |
| 幻读 | 可能* | 不可能 |
| 实现机制 | 单线程 | 锁机制 |
4.4 持久性
RDB持久化细节
-
触发机制:
- save 900 1(15分钟1次修改)
- save 300 10(5分钟10次修改)
- save 60 10000(1分钟10000次修改)
- 手动SAVE/BGSAVE命令
-
事务影响:
- 执行期间不阻塞事务
- 但可能丢失两次快照间的事务
- 建议配置:
stop-writes-on-bgsave-error yes
AOF持久化优化
- 写入策略对比:
| 配置 | 持久性保证 | 性能影响 | 适用场景 |
|---|---|---|---|
| appendfsync always | 最高 | 最大 | 金融交易 |
| appendfsync everysec | 中等 | 中等 | 大多数业务 |
| appendfsync no | 最低 | 最小 | 可丢失数据场景 |
- 重写机制:
- 自动压缩AOF文件
- BGREWRITEAOF触发
- 重写期间新命令写入缓冲区
混合持久化方案
推荐配置:
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # 混合模式
save 900 1 # RDB备份
优势:
- 快速恢复(RDB)
- 数据安全(AOF)
- 重写效率高(混合)
崩溃恢复流程
- 检查AOF文件完整性
- 加载RDB基础数据
- 重放AOF增量命令
- 数据一致性校验(可选)
五、Redis 事务的常见问题及解决方案
5.1 问题1:事务执行失败导致数据不一致
问题本质与复现
Redis 事务的"部分原子性"特性导致当遇到逻辑错误时,事务会部分执行而非整体回滚。这与传统关系型数据库的事务行为有显著差异。具体表现为:事务队列中某个命令执行失败时,Redis 会跳过该错误命令,继续执行后续命令,而不是回滚整个事务。
典型复现场景:
- 电商系统中同时执行库存扣减和订单创建
- 订单键被错误地设置为字符串类型,而业务代码却尝试使用哈希命令(HSET)
- 导致库存扣减成功但订单创建失败
详细复现步骤
# 1. 初始化库存(字符串类型)
127.0.0.1:6379> SET product:stock:1001 10
OK
# 2. 开启事务,包含错误命令
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY product:stock:1001 2 # 正确命令
QUEUED
127.0.0.1:6379(TX)> HSET order:3001 product_id 1001 quantity 2 # 错误命令(键类型不匹配)
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 8 # 库存扣减成功
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value # 订单创建失败
# 3. 验证数据状态
127.0.0.1:6379> GET product:stock:1001
"8" # 库存已扣减
127.0.0.1:6379> GET order:3001
(nil) # 订单未创建
解决方案:三层防护机制
方案1:事前检查键类型与业务规则
在事务执行前进行全面的数据校验:
- 检查键是否存在
- 验证键类型是否正确
- 检查业务数据是否满足条件(如库存是否充足)
Python实现示例:
def check_stock_validity(stock_key, reduce_num):
# 检查键类型
if r.type(stock_key) != b'string':
return False, "键类型错误"
# 检查库存充足性
current_stock = int(r.get(stock_key))
if current_stock < reduce_num:
return False, "库存不足"
return True, "校验通过"
# 使用示例
valid, msg = check_stock_validity("product:stock:1001", 2)
if not valid:
print(f"事务终止:{msg}")
else:
# 执行事务...
方案2:使用Lua脚本替代事务(推荐)
Lua脚本在Redis中的执行是原子性的,且支持复杂逻辑判断:
优势:
- 完整的原子性保证
- 支持条件判断和业务逻辑
- 执行效率更高
库存扣减Lua脚本示例:
-- 参数说明:
-- KEYS[1]: 库存键
-- KEYS[2]: 订单键
-- ARGV[1]: 扣减数量
-- ARGV[2]: 商品ID
-- 1. 数据校验
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock < tonumber(ARGV[1]) then
return {false, "库存不足"}
end
-- 2. 执行操作
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('HSET', KEYS[2], 'product_id', ARGV[2], 'quantity', ARGV[1])
return {true, "操作成功"}
方案3:事后主动回滚
当发生部分失败时,实现补偿机制:
实现要点:
- 记录操作日志
- 设计回滚逻辑
- 确保回滚操作的幂等性
Python回滚示例:
try:
# 执行事务
result = pipe.execute()
except Exception as e:
# 检查哪些操作已执行
if 'result' in locals() and result and isinstance(result[0], int):
# 执行回滚
r.incrby(stock_key, result[0])
print(f"已回滚库存,错误:{e}")
5.2 问题2:并发竞争引发脏数据
问题本质与复现
并发场景下多个客户端同时修改同一数据,导致数据不一致。典型表现为:
- 超卖(库存扣减为负数)
- 超扣(实际扣减量大于预期)
复现代码:
import threading
def reduce_stock():
current = int(r.get('stock'))
if current > 0:
r.decr('stock')
# 启动20个线程并发扣减初始为10的库存
threads = [threading.Thread(target=reduce_stock) for _ in range(20)]
[t.start() for t in threads]
[t.join() for t in threads]
print(r.get('stock')) # 可能为负数
解决方案
方案1:乐观锁(WATCH命令)
适用场景:并发量中等,冲突概率较低
实现步骤:
- WATCH监视关键键
- 获取当前值
- 在事务中执行修改
- 处理冲突(重试机制)
Python实现:
def optimistic_lock(key, max_retry=3):
for _ in range(max_retry):
try:
r.watch(key)
current = int(r.get(key))
if current <= 0:
break
pipe = r.pipeline()
pipe.multi()
pipe.decr(key)
if pipe.execute():
return True
except WatchError:
continue
return False
方案2:分布式锁
适用场景:高并发,强一致性要求
实现方案:
- 使用SETNX实现简单锁
- 考虑锁超时机制
- 实现锁续期
Python示例:
def acquire_lock(lock_name, acquire_timeout=10, lock_timeout=10):
end = time.time() + acquire_timeout
while time.time() < end:
if r.set(lock_name, 'locked', nx=True, ex=lock_timeout):
return True
time.sleep(0.1)
return False
def release_lock(lock_name):
r.delete(lock_name)
636

被折叠的 条评论
为什么被折叠?



