前言

  • 事务
    原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
    一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

    ③隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
    ④持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
    在Redis事务没有没有隔离级别的概念!
    在Redis单条命令式保证原子性的,但是事务不保证原子性!

  • 乐观锁
    ①当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。
    ②没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
    在Redis是可以实现乐观锁的!

Redis如何实现事务?

事务实现

Redis 事务通过三个命令进行控制。

  • muti:开启事务
  • exec:执行事务
  • discard:取消事务

①正常执行事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 26 #添加数据
QUEUED
127.0.0.1:6379> set high 172 #添加数据
QUEUED
127.0.0.1:6379> exec #执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379> get name #获取数据成功,证明事务执行成功
"dingyongjun"
127.0.0.1:6379> get age
"26"

②放弃事务

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 26 #添加数据
QUEUED
127.0.0.1:6379> discard #放弃事务
OK
127.0.0.1:6379> get name #不会执行事务里面的添加操作
(nil)

③编译时异常,代码有问题,或者命令有问题,所有的命令都不会被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 23 #添加数据
QUEUED
127.0.0.1:6379> getset name #输入一个错误的命令,这时候已经报错了,但是这个还是进入了事务的队列当中
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set high 173 #添加数据
QUEUED
127.0.0.1:6379> exec #执行事务,报错,并且所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name #获取数据为空,证明没有执行
(nil)

④运行时异常,除了语法错误不会被执行且抛出异常后,其他的正确命令可以正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加字符串数据
QUEUED
127.0.0.1:6379> incr name #对字符串数据进行自增操作
QUEUED
127.0.0.1:6379> set age 23 #添加数据
QUEUED
127.0.0.1:6379> get age #获取数据
QUEUED
127.0.0.1:6379> exec #执行事务。虽然对字符串数据进行自增操作报错了,但是其他的命令还是可以正常执行的
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "23"
127.0.0.1:6379> get age #获取数据成功
"23"

⑤总结:由以上可以得出结论,Redis是支持单条命令事务的,但是事务并不能保证原子性!

Redis 事务隔离机制

  1. 为什么需要隔离机制

​ 在并发场景下可能会出现多个客户端对同一个数据进行修改的情况。

​ 例如:有两个客户端 C 左与 C 右,C 左需要申请 40 个资源,C 右需要申请 30 个资源。它们首先查看了当前拥有的资源数量,即 resources 的值。它们查看到的都是 50,都感觉资源数量可以满足自己的需求,于是修改资源数量,以占有资源。但结果却是资源出现了“超卖”情况。

image-20250310174024040

​ 为了解决这种情况,Redis 事务通过乐观锁机制实现了多线程下的执行隔离。

  1. 隔离的实现

​ Redis 通过 watch 命令再配合事务实现了多线程下的执行隔离。

image-20250310174119208

以上两个客户端执行的时间顺序为:

时间C左C右
T1watch resources
T2get resourcesget resources
T3multi
T4decrby resources 40
T5decrby resources 30
T6exec
  1. 实现原理

其内部的执行过程如下:

  • 当某一客户端对 key 执行了 watch 后,系统就会为该 key 添加一个 version 乐观锁,并初始化 version。例如初值为 1.0。
  • 此后客户端 C 左将对该 key 的修改语句写入到了事务命令队列中,虽未执行,但其将该key 的 value 值与 version 进行了读取并保存到了当前客户端缓存。此时读取并保存的是version 的初值 1.0。
  • 此后客户端 C 右对该 key 的值进行了修改,这个修改不仅修改了 key 的 value 本身,同时也增加了 version 的值,例如使其 version 变为了 2.0,并将该 version 记录到了该 key信息中。
  • 此后客户端 C 左执行 exec,开始执行事务中的命令。不过,其在执行到对该 key 进行修改的命令时,该命令首先对当前客户端缓存中保存的 version 值与当前 key 信息中的version 值。如果缓存 version 小于 key 的 version,则说明客户端缓存的 key 的 value 已经过时,该写操作如果执行可能会破坏数据的一致性。所以该写操作不执行。

Redis如何实现乐观锁?

①watch(监视)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> set money 100  #添加金钱100
OK
127.0.0.1:6379> set cost 0 #添加花费0
OK
127.0.0.1:6379> watch money #监控金钱
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> DECRBY money 30 #金钱-30
QUEUED
127.0.0.1:6379> incrby cost 30 #花费+30
QUEUED
127.0.0.1:6379> exec #执行事务,成功!这时候数据没有发生变动才可以成功
1) (integer) 70
2) (integer) 30

②多线程测试watch

线程1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#线程1
127.0.0.1:6379> set money 100 #添加金钱100
OK
127.0.0.1:6379> set cost 0 #添加花费0
OK
127.0.0.1:6379> watch money #开启监视(乐观锁)
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> DECRBY money 20 #金钱-20
QUEUED
127.0.0.1:6379> INCRBY cost 20 #花费+20
QUEUED
#这里先不要执行,先执行线程2来修改被监视的值
127.0.0.1:6379> exec #执行报错,因为我们监视了money这个值,如果事务要对这个值进行操作前
#监视器会判断这个值是否正常,如果发生改变,事务执行失败!
(nil)

线程2

1
2
3
#线程2,这个在事务执行前操作执行
127.0.0.1:6379> INCRBY money 20 #金钱+20
(integer) 120

③总结:乐观锁和悲观锁的区别。

悲观锁: 什么时候都会出问题,所以一直监视着,没有执行当前步骤完成前,不让任何线程执行,十分浪费性能!一般不使用!
乐观锁: 只有更新数据的时候去判断一下,在此期间是否有人修改过被监视的这个数据,没有的话正常执行事务,反之执行失败!