Redis事务详解

Redis事务的定义知道吗?

答: redis的事务是一个单独隔离的操作,它会将一系列指令按需排队并顺序执行,期间不会被其他客户端的指令插队。

事务三大指令multi、exec、discard是什么?

答:

  1. multi:开启事务
  2. exec:执行事务
  3. 取消事务

如下图,通过multi,当前客户端就会开启事务,后续的指令都会安迅存到队列中。当用户键入exec后,这些指令都会按顺序执行。
若开启multi后输入若干指令,在键入discard,则之前的指令通通取消执行。

Redis事务详解_第1张图片

基础示例

开启事务并提交

# 开启事务
127.0.0.1:6379> MULTI
OK
# 将两个指令组队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
# 执行两个指令
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379>

事务的错误和回滚的情况了解嘛?

答: 分别由组队时错误和执行命令时错误两种情况

组队时错误

如下,我们在组队时输入错误的指令,redis会之间将所有指令都会失效,因为这是一个问题队列。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k33
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>

执行时错误

执行时错误比较特殊,他在按序处理所有指令,遇到错误就按正常流程处理继续执行下去。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> INCR k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379>

小结

为什么组队时出错和运行时出错会出现两种不同的情况呢?其实我们可以这样理解:

1. 组队时出错,错误对于redis来说是已知的,从设计者的角度出发,对于已知的错误我们需要提醒用户进行处理,所以就让事务中的所有指令都失效。
2. 运行时出错:因为错误是未知的,所以redis必须执行时才能知道错误,而redis也无错误回滚机制,所以就出现了将错就错,继续执行后续指令并有效的情况。

为什么redis需要事务呢?

答: 最经典的就是高并发导致超卖问题

超卖问题

如下图,假设一个秒杀活动中有3个用户,高并发场景下,执行以下步骤

	1. 都从数据库中查询到商品,若大于0开抢,小于等于0通知用户秒杀活动结束
	2. 有两个用户线程在此期间休眠
	3. 1人抢到商品,数据库扣为0
	4. 另外两个线程此时复活,由于休眠前查询到库存为1,也都执行抢产品的逻辑,导致库存最终变为-2,出现超卖问题

Redis事务详解_第2张图片

悲观锁

悲观锁(Pessimistic Lock) 认为自己操作的数据很可能会被他人修改,所以每次进行操作前都会对数据上锁,常见的关系型数据库MySQL的行锁、表锁等都是基于这种锁机制。

Redis事务详解_第3张图片

乐观锁

乐观锁(Optimistic Lock) 认为自己操作的数据不会被他人修改,当用户使用乐观锁锁住数据时,用户对拿到当前数据的版本号,修改完成后,会比较这个版本号和数据的版本号是否一致,若一致则说明别人没动过,提交修改操作。反之就是数据被他人动过,用户持有数据过期,提交失败。redis就是利用这种check and set机制实现事务的。

Redis事务详解_第4张图片

redis就是通过CAS(check and set)实现乐观锁的,通过watch指令监听一个或者多个key值,当用户提交修改key值的事务时,会检查监听的key是否发生变化。若没有发生变化,则提交成功。

下面我们就来演示一下乐观锁,首先redis设置一个名为key的值,并且对这个客户端上锁

# 刷新数据库
127.0.0.1:6379> FLUSHDB
OK
# 设置key值
127.0.0.1:6379> set key 10
OK
# 监听key
127.0.0.1:6379> WATCH key
OK
# 开启事务
127.0.0.1:6379> MULTI
OK

客户端2同样开启监听并开启事务

# 监听key
127.0.0.1:6379> WATCH key
OK
# 开启事务
127.0.0.1:6379> MULTI
OK

客户端1提交修改

# 指令加入队列
127.0.0.1:6379(TX)> INCR key
QUEUED
# 执行指令,可以看到执行成功,修改了一条数据,值被更新为11
127.0.0.1:6379(TX)> EXEC
1) (integer) 11

回到客户端2指令组队并提交,可以看到提交结果失败了,返回nil

127.0.0.1:6379(TX)> INCR key
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

可以上到两个客户端同时监听一个key值,第一个客户端修改后,第2个客户端的修改就无法成功提交,说明redis的watch是基于乐观锁机制的。

Redis事务详解_第5张图片

为什么redis不支持事务回滚

答:

  1. redis实际上是支持事务回滚的,只不过这种回滚是发生在指令组队阶段,因为这些指令是可以预知的。
  2. 对于运行时出错,redis是不支持回滚的,因为情况是未知的,所以为了保证redis简单快速,所以设计者并未将运行时出错的事务回滚。

如何理解redis的事务与ACID

答:

  1. 原子性: redis设计者认为他们是支持原子性的,因为原子性的概念是:所有指令要么全部执行,要么全部不执行。而非一起成功或者失败。
  2. 一致性: redis事务保证命令失败(组队时出错)的情况下可以回滚,确保了一致性。
  3. 隔离性: redis是基于单线程的,所以执行指令时不会被其他客户端打断,保证了隔离性。但是redis并没有像其他关系型数据库一样设计隔离级别。
  4. 持久性: 持久性的定义为事务处理结束后,对数据的修改就是永久的即便系统故障也不会丢失。),考虑到性能问题,redis无论rdb还是aof都是异步持久化,所以并不能保证持久性。

Redis事务的其他实现方式了解过嘛?

答: 基于lua脚本可以保证redis指令一次性执按顺序执行完成,并且不会被其他客户端打断。我们可以将其想象为redis实现悲观锁的一种方式,但是这种方式却无法实现事务回滚。

Redis事务三特性是什么?

答:

  1. 单独的隔离操作:事务中的命令都会序列化并且按序执行,执行过程中不会被其他客户端的指令打断。
  2. 没有隔离级别的概念: 事务提交前所有指令都不会被执行。
  3. 无原子性:上文示例已经演示过,执行时出错某段指令,事务过程中的指令仍然会生效。

面试题

如何使用 Redis 事务?

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。

Redis 支持原子性吗?

redis事务出错不回滚使得很多人认为redis事务是未被原子性这一原则的,实际上redis设计者不这么认为,因为redis设计者认为原子性的定义为:所有指令要么一起执行,要么全都不执行,而不是完全成功。

如何解决 Redis 事务的缺陷?

从上文我们看出基于redis事务进行秒杀方面的需求时会出现库存遗留问题,这就是redis事务乐观锁机制的缺陷。
为了保证所有事务都能一次性的执行,我们可以使用lua脚本更快(lua脚本可以轻易调用C语言库函数以及被C语言直接调用)、更有效(基于lua脚本可以保证指令一次性被执行不会被其他线程打断),但是这种方案不支持回滚。

小结

Redis进阶 - 事务:Redis事务详解

Redis常见面试题总结(下)

你可能感兴趣的:(数据库,redis,java,数据库)