1. Redis与MySQL的事务差别
相信一谈到事务,大家马上就能联想到MySQL的事务,其事务具有ACID四大特性,但是Redis的事务相比较于MySQL,那就是个"弟中弟",下面我们就来简单对比两者的事务特性:
- 原子性:关于Redis事务是否具备"原子性"是存在争议的,Redis的原子性最原始的含义就是指"把一组操作打包在一起执行,要么全都执行;要么全都不执行"(但是Redis并没有回滚操作,即若中间有操作执行失败,Redis也会继续执行下去),但是对于MySQL来说,其提供的"原子性"特性保证这组操作要么全都执行成功,要么全都不执行(即MySQL事务带有回滚机制)
- 一致性:Redis的事务不存在一致性,因为没有提供回滚机制,因此若中间操作执行失败,就可能存在数据不一致的情况
- 持久性:Redis中的事务不存在持久性,尽管Redis含有持久化机制,但是这与事务特性无关
- 隔离性:Redis中的事务不涉及到隔离性,因为Redis是一个单线程的服务器模型,所有的请求/事务都是串行执行的
2. Redis事务概述
目的 :Redis中的事务最根本的目的就是将一组操作打包在一起执行,中间不允许有别的客户端插队执行
举一个形象生动的例子,我和我的朋友一起去吃烧烤,我一开始点了一堆五花肉、羊肉、牛肉、猪肉等菜品,但是我的朋友还没到,于是我让老板先存单不着急烤,当我的朋友到了以后他又点了一堆猪腰子、生蚝等菜品。此时我们告诉老板可以烤了(在这里我们前后两堆菜品一定是需要放在一起烤的,不会出现我们的菜品跟别的客人放一起端上来的情况)
实现方式:Redis服务器为每个客户端维护了一个队列(先进先出),当"事务开启"之后,客户端发送的命令就会加入到服务端对应的队列上而不是立即执行,如果遇到相应客户端的"执行事务"命令则按顺序将队列中的操作进行执行,并且根据原子性,只有处理完一个客户端的队列操作后才能执行下一个客户端的事务
💡 思考一下:为什么Redis中的事务如此简单,不设计的跟MySQL一样强大呢?
- 在空间上,需要额外的空间存储相应的更新操作(如undolog、redolog等等)
- 在时间上,回滚操作也需要额外的执行性能开销
而我们Redis是基于内存的数据库,追求性能!
应用场景:我们用到Redis中的事务操作往往在一些需要将一组操作放在一起执行的场景中,比如说秒杀场景中,提供下列伪代码,如果不加以任何限制让多个客户端并发执行,就可能存在超卖的问题!在Redis中我们就可以通过事务的方式来解决这种问题!
java
get count;
if (count > 0) {
// 执行下单操作
decr count;
}
我们借助事务操作就可以实现类似如下功能:
java
开启事务
get count;
if (count > 0) {
// 执行下单操作
decr count;
}
执行事务;
这样一来,当服务器接收到第二个客户端的"执行事务"命令时,第一个客户端的事务已经执行完毕,此时第二个客户端获取到的count就是正确的,也就避免了超卖问题。但是这里我们还需要想一想,Redis如何支持if条件判断语句呢?由于Redis支持原生Lua脚本,可以搭配Lua脚本进行if条件判断(感兴趣的小伙伴自行了解)
3. Redis事务操作
开启事务: MULTI
执行事务: EXEC
放弃当前事务: DISCARD
我们使用MULTI
开启事务,并添加了两个key,但是并没有执行事务,此时如果使用第二个客户端进行访问,获取不到k1与k2的值:
但是当我们使用EXEC
执行事务后,第二个客户端就可以进行访问了
温馨提示:如果在事务开启之后,突然断电,相当于执行了DISCARD丢弃当前事务操作!
4. watch监视原理
我们可以使用WATCH
命令用来监视某个key是否在事务执行之前发生了变化:
在当前客户端上我们先使用watch k1
监视k1键,此时第二个客户端执行了set k1 111
此时当第一个客户端执行事务后,发现k1已经被改了于是返回nil,不执行set k1 222
命令。
watch实现原理 :类似于乐观锁,即预估当前发生锁冲突的概率不是很高,加锁操作的成本也比较低
watch中的乐观锁是基于 "版本号 " 这样的机制实现的:
- 一开始客户端1执行
watch k1
时就会给k1分配一个版本号(递增的数字),例如为1,之后使用MULTI开始事务 - 这时客户端2执行
SET k1 111
修改了k1,此时版本号增长为2(由于客户端1此时还没有执行EXEC命令,因此由客户端2先执行) - 此时客户端1执行
EXEC
命令,首先判断当时记录的k1的版本号是否与当前版本号一致,如果不一致说明有其余客户端更改,则返回nil,不执行事务中的内容;如果一致则继续执行
这个思想方法也与CAS的ABA问题解决方式相类似