五.Redis事务与乐观锁

本文最后更新于:2022年1月13日 下午

Redis事务

Redis事务本质:一组命令集合。一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行。

1
|------队列 set set set....队列-------|

Redis事务:

  • 开启事务(multi)
  • 命令入队列(一系列命令)
  • 执行事务(exec)

所有的命令在事务中,并没有直接执行,而是只会在执行命令发起的时候才会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#正常执行命令
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 v1
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec //事务执行,依次按顺序执行命令
1) OK //依次的执行结果
2) OK
3) "v1"
4) "v1"
#取消一个事务 discard命令 执行此命令后,事务将被放弃,队列将被清空,且将会从事务状态中退出
127.0.0.1:6379(TX)> set k1 1
QUEUED
127.0.0.1:6379(TX)> set k2 2
QUEUED
127.0.0.1:6379(TX)> set k4 4
QUEUED
127.0.0.1:6379(TX)> discard //事务取消之后,事务中所有的命令都不会被执行
OK
127.0.0.1:6379> get k4
(nil) //因为事务被取消,k4并没有set,因此k4为nul
127.0.0.1:6379>

Redis中的两种错误:(同java基本一致)

  • 编译型异常(代码有问题,命令语法错误):事务中的所有命令都不会被执行!(入队失败)
  • 运行时异常(分母为0,数组越界等):如果事务队列中的某一条命令存在运行时异常,那么在执行事务时,其他命令是可以正常执行的,错误的命令会抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#编译型异常
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set v1 1
QUEUED
127.0.0.1:6379(TX)> setset v1 2
(error) ERR unknown command `setset`, with args beginning with: `v1`, `2`,
127.0.0.1:6379(TX)> set v2 2
QUEUED
127.0.0.1:6379(TX)> exec //由于存在语法错误,队列中的所有命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.

#运行时异常
127.0.0.1:6379(TX)> set v1 "ab"
QUEUED
127.0.0.1:6379(TX)> incr v1 //对字符串+1属于运行时异常
QUEUED
127.0.0.1:6379(TX)> set k2 22
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range //执行时报错
3) OK
4) "22" //其他命令无异常,正常执行
127.0.0.1:6379>

Redis事务并没有隔离级别的概念

重点:Redis中单条命令是原子性的,但事务并不保证原子性(acid没有全部实现,与关系型数据库不同),一旦事务中(命令队列中)有一条命令执行失败(运行时异常),并不影响整个事务的执行,而mysql的事务具有原子性,在一个事务中,多条命令,一旦有一条执行失败,其他的也无法成功执行。

一次性,顺序性,排他性地执行一系列命令

  • exec命令执行之前,多个命令被放入队列缓存中
  • exec命令执行后,缓存队列中的命令顺序执行,一旦有一个错误(运行时异常),不影响其他命令的执行
  • 事务执行过程中,其他客户端提交的命令请求不会插入到当前的缓存命令队列中,这里只是保证在执行过程中,在将命令添加进入队列的过程中,是不会保证不被其他客户端命令插入的
  • 不支持回滚,没有实现发生错误直接回滚的功能,redis的事务更像一个命令打包的功能

为什么不支持回滚?官方回答:

1
只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。

Redis实现乐观锁

  • 悲观锁:见名知意,很悲观,担心数据会被修改,对读写操作都上锁,自己用完数据后,就会进行解锁。那么其它线程进行操作时必须等待,效率相对较低。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等。都是在操作之前先上锁让别人无法操作该数据。
  • 乐观锁:顾名思义,很乐观,每次取数据的时候并不担心自己的数据会被修改。数据库实现乐观锁,通常使用的是version(数据版本),来表示数据的。每次取数据的时候,连同数据版本version一起取出。当读取出数据,对数据进行更改时,会将数版本version 加一,然后提交到数据库,此时比较数据版本version,如果提交的数据版本version大于当前数据库对应记录的数据版本version,那么提交成功。否则小于等于都会提交失败。需要重新从数据库取数据。
  • 乐观锁适用场景:频繁读取数据的场景,因为读取数据并不会上锁。但是当有大量数据写入的时候,会频繁的提交不成功,会重新读取数据,再提交。
  • 悲观锁适用场景:频繁写入数据的场景,因为不管是读还是写 都会上锁,如果大量写入数据,为了数据安全上锁是有必要的,相反乐观锁就会大量的读取提交操作。但是当有大量数据读出的时候,效率低下。

Redis实现乐观锁:

1

​ Redis通过使用watch命令来实现乐观锁,watch命令用于监视一个或多个key,如果在事务执行之前,这个(或这些)key被其他命令所改动,那么这个事务将被打断。

​ 大多数是基于数据版本(version)的记录机制实现的。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个”version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据。

“ Redis使用WATCH命令实现事务的“检查再设置”(CAS)行为。作为WATCH命令的参数的键会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个Null值,提醒用户事务运行失败。”

​ 如果在添加watch之后,事务正常exec完成,会自动释放锁(监视),其他的情况,需要手动执行unwatch命令释放监视。

​ watch的key是对整个连接有效的,事务也一样(但事务被执行后就不存在了),如果连接断开,监视和事务都会被自动清除,discard和unwatch命令也会清除连接中的所有监视。

​ 操作实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#正常情况下,设置监视
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
#监视money对象
127.0.0.1:6379> watch money
OK
#事务正常结束,数据期间没有改动,这时正常执行成功
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

#多个客户端的场景下
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
#执行之前,开启另一个线程去修改money的值,这个时候就会导致事务执行失败。
127.0.0.1:6379> exec
(nil)

#出现上述错误后,需要手动解锁,并重新获取锁
#如果发现事务执行失败就先解锁
127.0.0.1:6379> unwatch
OK
#获取最新的值再次监控,类似select version
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 1000
QUEUED
#执行之前会先比较version ,一样就会执行成功,反正失败
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 1000
127.0.0.1:6379>

本文作者: ziyikee
本文链接: https://ziyikee.fun/2021/11/16/%E4%BA%94-Redis%E4%BA%8B%E5%8A%A1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!