概述
Redis 事务是一种将多个命令打包在一起执行的机制。通过使用事务,可以确保一系列命令在一次执行中依次执行,而不会被其他客户端的命令请求打断。
Redis 事务的执行分为以下几个步骤:
- 开启事务 :使用 MULTI 命令开启一个事务。之后的命令都会被添加到事务队列中,而不会立即执行。
- 添加命令 :在 MULTI 和 EXEC 命令之间,可以添加任意数量的 Redis 命令,这些命令会按照添加的顺序被放入事务队列中。
- 执行事务 :使用 EXEC 命令执行事务。Redis 会按照事务队列中命令的顺序依次执行这些命令。在执行事务期间,Redis 不会处理其他客户端的命令请求。
- 事务结果 :执行事务后,Redis 会返回事务中每个命令的执行结果。结果的顺序与命令在事务队列中的顺序相对应。
- 取消事务 :在开启事务后,可以使用 DISCARD 命令取消事务,丢弃事务队列中的所有命令。
Redis 事务的一个重要特性是乐观锁。在执行事务期间,Redis 不会对数据进行锁定或阻塞其他客户端操作,而是在执行事务时记录了事务执行期间所依赖的键的状态,如果在执行 EXEC 命令之前检测到这些键的状态发生了变化,那么事务将会被中断并放弃执行。
需要注意的是,Redis 的事务是原子性的,要么全部执行成功,要么全部放弃执行。如果在事务执行期间出现错误,事务中的所有命令都不会被执行,并且不会对数据产生任何影响。Redis 事务的应用场景包括批量操作、原子操作、队列操作等,通过将多个命令打包在一起执行,可以减少网络通信开销,提高性能,并保持一致性。
乐观锁和悲观锁
悲观锁 :
- 悲观锁的思想是,在整个数据操作过程中,将数据进行加锁,以阻止其他事务或线程对数据进行修改。
- 当一个事务获取了悲观锁后,其他事务需要等待锁被释放才能继续操作,从而保证了数据的一致性。
- 悲观锁适用于并发冲突较多、写操作频繁的场景。
- 在关系型数据库中,使用行级锁或表级锁实现悲观锁。
乐观锁 :
- 乐观锁的思想是,假设多个事务之间不会发生冲突,每个事务在修改数据时不会阻塞其他事务,只在提交时检查是否有冲突。
- 乐观锁通过在数据记录中添加版本号或时间戳等标识,用于检测数据是否被其他事务修改过。
- 当一个事务提交时,会比较当前数据的版本号与事务开始时获取的版本号是否一致,如果一致则提交成功,否则表示数据已被其他事务修改,需要进行冲突处理。
- 乐观锁适用于并发冲突较少、读操作频繁的场景,避免了数据的阻塞等待。
- 在关系型数据库中,使用版本号或时间戳实现乐观锁。
在实际应用中,选择使用悲观锁还是乐观锁取决于具体的需求和场景。悲观锁可以确保数据的一致性,但可能带来较高的开销和性能影响。乐观锁则通过乐观的假设减少了锁的竞争,提高了并发性能,但需要在冲突发生时进行额外的处理。
事务的错误处理
出现语法错误时,EXEC 后会直接返回错误,语法正确的命令也不会执行
代码语言:javascript复制127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> SET name cx
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
出现运行错误时,比如使用集合的命令操作字符串,这种错误在执行之前无法被发现,所以这种情况下正确的命令会被执行。Redis 的事务机制并不支持事务回滚(rollback)操作,因此当出现事务运行错误需要自己收拾烂摊子。
代码语言:javascript复制127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET name caixing
QUEUED
127.0.0.1:6379> SADD name cx
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
WATCH 命令
WATCH 命令用于实现乐观锁机制,它可以在事务执行之前监视一个或多个键。WATCH 命令会监视指定的键,如果在执行事务期间有其他客户端修改了被监视的键,那么当前事务将被打断,不会执行,可以通过检查执行 EXEC 命令的返回值来判断是否执行了事务。
使用 WATCH 命令监视一个或多个键:
代码语言:javascript复制WATCH key1 key2 ...
可以指定一个或多个键来监视。一旦执行了 WATCH 命令,Redis 会将这些键标记为被监视状态。
比如说如果要自己利用 GET 和 SET 实现 INCR 函数,伪代码如下所示。这段代码可能出现竞态条件,key 的原始值为 6,期望情况应该是将 key 变为 8,但是如果两个客户端同时(在对方尚未返回时)开始执行这段代码,那么最后 key 是 7。
代码语言:javascript复制$val = GET $key
if $val is null
$val = 0
$val = $val 1
SET $key $val
虽然事务提供了原子性,但是由于事务每条命令的执行结果是同时返回的,所以不适用这种前后具有依赖关系的命令,为了解决这个问题,可以使用 WATCH 来实现 GET 获得键值后不允许其他客户端修改键以防止竞态条件。
在执行事务前使用 WATCH 监视 key,在执行事务前 key 发生了改变,变成了 3,因此事务未执行。WATCH 的监控一直持续到 EXEC 命令。
代码语言:javascript复制127.0.0.1:6379> SET key 1
OK
127.0.0.1:6379> WATCH key
OK
127.0.0.1:6379> SET key 2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key 3
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> GET key
"2"
修改刚刚的伪代码如下。
代码语言:javascript复制WATCH $key
$val = GET $key
if $val is null
$val = 0
$val = $val 1
MULTI
SET $key $val
EXEC