事务特点
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
-
批量操作在发送 EXEC 命令前被放入队列缓存。
-
收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。不具备原子性。
-
在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis 事务的缺点
- 不支持回滚: Redis事务在执行
EXEC
命令之前,如果通过WATCH
检测到监视的键发生了变化,则会拒绝执行整个事务并返回空结果。但请注意,这并不是传统数据库中的“回滚”操作,因为Redis不会自动撤销已经执行过的命令,它仅仅是在事务中止时阻止后续未执行的命令。 - 命令排队一次性执行: 在Redis事务中,所有命令会被放入一个队列,在
EXEC
命令被执行时按照先进先出的顺序执行,期间不能中断或插入新的命令。这意味着事务开始后无法根据中间结果动态调整事务内的操作,降低了灵活性。 - 无隔离级别: Redis的事务没有提供如SQL数据库那样的多种事务隔离级别(如读已提交、可重复读等)。所有事务都是在单线程环境下的串行化执行,因此避免了脏读、不可重复读等问题,但这也意味着在高并发场景下可能会有性能瓶颈。
- Watch-Multi-Exec模式的问题: 使用
WATCH
进行乐观锁控制时,一旦网络延迟或者客户端异常导致事务未能及时执行,监视的数据可能已经被其他客户端修改,此时即便事务最终执行,也无法保证数据一致性。 - 批量操作不具备完全的原子性: 虽然Redis的所有命令在服务器内部是原子执行的,但在一个事务中多个命令的组合并不能视为一个原子操作。例如,事务中包含对多个key的操作,即使其中一个操作失败,事务内其它命令也会被执行完毕,而不是整体取消。
三个阶段
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
案例步骤
从multi 开始,一系列操作,最后执行 exec ,批量执行处理
127.0.0.1:6379> multi
OK
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> hset person name zhang age 34
QUEUED
127.0.0.1:6379> hdel person age
QUEUED
127.0.0.1:6379> hset person email zhang@sina.com
QUEUED
127.0.0.1:6379> exec
1) 1) "listz"2) "book"3) "word"4) "myset"5) "mydest"6) "student"
2) (integer) 2
3) (integer) 1
4) (integer) 1
127.0.0.1:6379> hgetall person
1) "name"
2) "zhang"
下表列出了 redis 事务的相关命令:
序号 | 命令及描述 |
---|---|
1 | [DISCARD] 取消事务,放弃执行事务块内的所有命令。 |
2 | [EXEC] 执行所有事务块内的命令。 |
3 | [MULTI] 标记一个事务块的开始。 |
4 | [UNWATCH] 取消 WATCH 命令对所有 key 的监视。 |
5 | [WATCH key [key ...]] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。 |
乐观锁
乐观锁(Optimistic Locking)是一种在数据库并发控制中的策略,它假设多用户同时访问同一数据时发生冲突的概率较低,并且在更新数据之前并不立即进行加锁操作。与悲观锁不同的是,悲观锁在读取数据时就直接获取并持有锁,直到事务结束才释放;而乐观锁则是:
- 读取阶段:当一个事务想要修改数据时,它不会立即锁定该数据行。每个事务在读取数据时都会记录下当时的数据版本号或时间戳等信息。
- 验证阶段:在事务提交更新操作前,会再次检查当前要更新的数据是否自上次读取以来没有被其他事务修改过。这通常通过比较数据的版本号来实现,如果版本号未变,则认为可以安全地执行更新。
- 更新阶段:如果数据版本验证通过(即版本号仍为事务开始读取时的版本),则执行更新操作,并将数据版本号递增,确保后续的并发事务能够识别出这次更新。如果发现版本号已被改变,说明存在并发修改,此时乐观锁机制会让当前事务回滚,并提示并发错误,通常需要重新读取数据并尝试更新。
Redis 乐观锁
Redis 乐观锁是一种在分布式系统中实现并发控制的机制,它借鉴了数据库领域的乐观并发控制思想,并通过Redis提供的命令来实现。在乐观锁策略下,假定多个客户端同时访问同一数据时,通常不会发生冲突或至少冲突的概率较低。因此,在读取数据时不立即加锁,而是在更新数据前才去检查在此期间是否有其他客户端修改过该数据。
在Redis中,乐观锁主要通过WATCH
命令和事务(multi/exec)来实现:
- WATCH命令:客户端使用
WATCH
命令监视一个或多个键,这些键的数据状态将被记录下来。当执行WATCH
后,如果任何被监视的键在事务提交前发生了变化,则整个事务将会被打断,即不会执行EXEC
命令内的操作。 - 事务处理:客户端可以将一系列命令放入事务中,使用
MULTI
开始一个事务块,然后执行一系列的操作指令。最后,用EXEC
命令尝试提交事务。只有在所有被WATCH
的键自WATCH
以来未被其他客户端改变的情况下,事务中的命令才会被执行。
举例来说:
- 客户端A对一个键进行
WATCH
。 - 然后客户端A开始一个事务,并准备修改这个键的值。
- 在事务提交(
EXEC
)之前,如果其他客户端改变了该键的值,那么客户端A的事务在执行EXEC
时会发现数据已经被修改,从而导致事务回滚,不执行任何操作。
下面具体来举例:
主要是 watch 和 multi 两个命令配合使用,实现乐观锁
watch 监视某一个可能变化的 key。然后开启事务,一系列操作放入队列等待执行,如果在exec 执行事务之前,其他的客户端对监视的key 做了修改,则exec 执行结果为nil 。什么都不执行。
127.0.0.1:6379> watch mm #### 开始监视 mm 变量
OK
127.0.0.1:6379> multi #### 开启事务
OK
127.0.0.1:6379> incrby mm 500 #### 第一次 给 mm 加500 操作放入队列
QUEUED
127.0.0.1:6379> incrby mm 500 #### 第二次 给 mm 加500 操作放入队列
QUEUED
### 此时去另一个客户端执行修改操作
另一个客户端的修改操作
127.0.0.1:6379> decrby mm 500 #### 给 mm 减去500
(integer) -500 #### 立即起效,结果为 -500
然后再回原来执行事务的客户端执行下面操作:
127.0.0.1:6379> exec #### 开始执行事务
(nil) #### 没有任何执行结果 因为乐观锁起作用了
127.0.0.1:6379> get mm #### 再次查看结果
"-500" #### 是另一个客户端的执行结果。刚才加的两次500 无效。
127.0.0.1:6379> watch mm #### 再次监控
OK
127.0.0.1:6379> multi #### 开启事务
OK
127.0.0.1:6379> incrby mm 500 #### 加500
QUEUED
127.0.0.1:6379> incrby mm 500 #### 加500
QUEUED
127.0.0.1:6379> exec #### 执行事务
1) (integer) 0
2) (integer) 500 #### 执行成功,因为其他客户端没有修改被监视的变量 mm .
127.0.0.1:6379>