如果你对Redis总体上没有认识,建议先看看我之前的博客,对Redis有详细的介绍。
和面试官从NoSql谈起【Redis】
【Redis】的五大基本类型
前言:
大白头同学今天问我,说她最近在使用Redis做缓存,问我Redis有事务管理吗?我的第一意识就是:既然Redis是数据库,那么它就肯定像MySQL一样是有事务的。大白头同学又问Redis中的事务也能保证原子性吗?我说不能。但是为什么不能,我一时语塞不能回答出来。
Redis中有事务吗?
Redis中可以实现事务,也就是能够将多条命令(多个操作)变成一个操作去执行。但是Redis中事务不保证原子性
Redis事务的本质:一组命令的集合
原因:
Redis中的事务其实是Redis中命令的组合,Redis中单条命令是保证原子性的,但是事务中组合多条命令的时候并不能保证原子性。也就是说,多条命令组合的事务在Redis中如果前面的命令正确执行,但是后面的命令出错(这里指的是运行时错误),那么前面的命令是可以正常执行的,而后面的命令是没有执行的。
画个图简单理解一下:
一个事务中的所有命令会被序列化,在事务执行过程中,按照顺序执行
Redis事务的特性:
一次性,顺序性,排他性。
Redis的事务就好像一个队列,多个Redis的命令在队列中排列,一个一个出队执行,因此前面的执行成功,后面的不一定执行成功,后面的命令即使执行失败,也不会对已经执行的命令造成影响。
Redis的事务如何执行
注意:事务在执行之后需要再次开启事务,一个事务是不能重复利用的
- 开启事务:
multi
- 命令入队:Redis自动压入队
- 执行事务:
exec
mylocalhost:0>multi # 开启事务
"OK"
mylocalhost:0>set k1 v1 # 命令入队
"QUEUED"
mylocalhost:0>set k2 v2 # 命令入队
"QUEUED"
mylocalhost:0>set k3 v3 # 命令入队
"QUEUED"
mylocalhost:0>exec # 执行事务
1) "OK"
2) "OK"
3) "OK"
4) "OK"
5) "OK"
6) "OK"
7) "OK"
mylocalhost:0>get k1
"v1"
mylocalhost:0>get k2
"v2"
mylocalhost:0>get k3
"v3"
- 除了在开启事务之后可以执行事务以外,也可以放弃事务,不去执行事务:
discard
一旦放弃事务,那么事务队列中的所有命令都不会执行,这也从侧面说明了Redis的事务是在调用执行命令的时候才真正执行(也就是出队),而不是在命令添加到队列的时候
Redis事务中如果有错误,Redis如何处理?
在编写Redis事务时,可以会导致编写的事务命令是错误,这种错误大致可以分为两类,编译时错误和运行时错误,了解Redis对于这两种错误的处理方式,能更加合理的使用Redis(首先你就知道了Redis在发生错误时,到底做了什么事)。
编译时错误
Redis的编译时错误指的是:代码错误,也就是命令有错。事务中所有的命令都不会被执行
mylocalhost:0>multi
"OK"
mylocalhost:0>set k1 v1
"QUEUED"
mylocalhost:0>set k2 v2
"QUEUED"
mylocalhost:0>getset k2 #命令出错
"ERR wrong number of arguments for 'getset' command"
mylocalhost:0>set k3 v3
"QUEUED"
mylocalhost:0>get k3
"QUEUED"
mylocalhost:0>exec # 导致所有的命令都没有执行
"EXECABORT Transaction discarded because of previous errors."
运行时错误
运行时错误(代码逻辑错,比如像1/0)。事务中除了该出错命令以外,其他命令是能正常执行的
mylocalhost:0>set k1 "str" #在事务开启之前,我们将k1设置为字符串tr
"OK"
mylocalhost:0>multi #开启事务
"OK"
mylocalhost:0>incr k1 # k1自增1 这里的代码会运行出错,因为自增只能是数字类型的
"QUEUED"
mylocalhost:0>set k2 v2
"QUEUED"
mylocalhost:0>set k3 v3
"QUEUED"
mylocalhost:0>get k3
"QUEUED"
mylocalhost:0>exec #执行事务
1) "OK"
2) "ERR value is not an integer or out of range" # 该命令执行出错
3) "OK" # 其余的代码都能正常执行
4) "OK"
5) "OK"
6) "OK"
7) "OK"
8) "v3"
9) "OK"
Redis实现乐观锁
什么是悲观锁?
悲观锁,顾名思义就是很悲观,当前线程在执行事务时,总感觉其他事务也会执行(可能会导致线程安全问题),因此它就给自己的操作上锁,只允许当前线程访问,当当前线程访问结束之后才让其他线程访问。
什么是乐观锁?
乐观锁,顾名思义就是很乐观,认为自己访问的时候其他线程是不会访问的,因此不加锁,但是如果保证数据的一致性呢?是通过对照之前的版本(类似于之前在MySQL中学习的version和JUC中的CAS思想)。
Redis是可以实现乐观锁的。使用的是监控的方式。
local:0>set money 100 #设置初始金钱100
"OK"
local:0>set out 0 #设置初始消费0
"OK"
local:0>watch money # 监控金钱
"OK"
local:0>multi #开启事务 在事务执行期间没有其他线程改变金钱100,那么事务正常执行
"OK"
local:0>decrby money 20 # 金钱-20
"QUEUED"
local:0>incrby out 20 #消费+20
"QUEUED"
local:0>exec # 执行事务
1) "OK"
2) "80"
3) "OK"
4) "20"
5) "OK"
模拟多线程下的Redis事务
第一个客户端
local:0>set money 100 # 设置初始金币100
"OK"
local:0>set money 20 #设置初始花费20
"OK"
local:0>watch money #监控金钱
"OK"
local:0>multi #开启事务
"OK"
local:0>decrby money 20 # 金钱-20
"QUEUED"
local:0>incrby out 20 # 消费+20
"QUEUED"
第二个客户端
mylocalhost:0>set money 1000 # 在第一个客户端开启事务,并启动监控时,修改了金钱的值
"OK"
第一个客户端
local:0>exec # 执行事务,发现没有输出,也就是没有执行任何事务中的操作
local:0>get money
"1000"
local:0>unwatch #放弃监视之后,如果后续还需要对该数据进行操作,那么就用watch去重新监视 然后去执行
"OK"
local:0>watch money #重新开启了监视
"OK"
local:0>multi #开启事务
"OK"
local:0>decrby money 20 #金币-20
"QUEUED"
local:0>incrby out 20 #消费-20
"QUEUED"
local:0>exec #执行事务,发现执行成功
1) "OK"
2) "980"
3) "OK"
4) "20"
5) "OK"
其实这就是乐观锁的思想:在当前线程执行时,发现了别的线程对这个数据进行了修改,那么我们这次就不对这个数据进行操作,而是获取新的数据后然后再对数据进行修改,很类似CAS的思想。是一种自旋的思想。