Redis 事务揭秘:如何确保数据一致性
Hello , 大家好 , 这个专栏给大家带来的是 Redis 系列 ! 本篇文章给大家讲解的是 Redis 中的事务 . Redis 的事务是一个用于将多个命令打包在一起执行的特性 , 它可以保证这些命令要么全部执行 , 要么全部不执行 , 从而确保数据的一致性 . 我们可以看一下 Redis 中的事务是如何实现的
本专栏旨在为初学者提供一个全面的 Redis 学习路径,从基础概念到实际应用,帮助读者快速掌握 Redis 的使用和管理技巧。通过本专栏的学习,能够构建坚实的 Redis 知识基础,并能够在实际学习以及工作中灵活运用 Redis 解决问题 .
专栏地址 : Redis 入门实践
一 . 认识 Redis 事务
我们之前学习过 MySQL 的事务
- 原子性 : 把多个操作打包成一个整体
- 一致性 : 事务执行之前和事务执行之后数据要一直正确
- 持久性 : 事务做出的修改都会保存到硬盘中
- 隔离性 : 事务并发执行涉及到的一些问题
相比于 MySQL 来说 , Redis 的事务就简单了很多
- 弱化了原子性 : Redis 有无原子性存在争议 , 是因为 MySQL 将原子性提高了门槛
原子性最原本的含义指的是把多个操作打包成一个整体 , 要么全部执行 , 要么全部不执行 .
那 Redis 也确实做到了这样的要求 , 但是相比之下 MySQL 的原子性要求更加严格 , MySQL 要求要么全部执行成功 , 要么全都不执行 .
Redis 是不能保证执行成功或者执行正确的 , 如果事务中若干个操作存在失败的事务 , 那就这么地了 , 而 MySQL 中有事务执行操作失败 , 那就需要进行回滚 , 将已经执行的操作也需要全部进行回退
- 不具有一致性 : 由于 Redis 并没有回滚机制 , 也没有相关约束 , 那事务执行过程中如果某个修改操作出现失败 , 就可能引起不一致的情况
- 不具有持久性 : Redis 是一个内存数据库 , 虽然也有持久化机制 , 但是 Redis 可以禁用持久化机制
- 不涉及隔离性 : Redis 是一个单线程模型的服务器程序 , 所有的请求都是串行执行的
那 Redis 的事务存在的意义 , 就是为了 “打包” 操作 , 避免其他客户端的命令插队执行 . 在 Redis 中实现事务 , 是引入了一个队列 , 这个队列是每个客户端都有的 .
开启事务的时候 , 此时客户端输入的命令 , 就会发送给服务器并且保存到队列中 (并不是立即去执行)
当执行事务的时候 , 此时就会把队列中的这些任务按照顺序依次执行 (依次执行都是通过 Redis 主线程中完成的 , 主线程会把事务中的操作全部执行完 , 再去处理别的客户端的操作)
二 . 事务相关的命令
开启事务 : multi
执行事务 : exec
放弃事务 : discard
我们具体操作一下
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 111 # 设置键值对
# 但是目前并不是真正的保存到了 Redis 中
# 只是存放到了服务器存放事务的队列中 , 保存了一系列请求
QUEUED
127.0.0.1:6379> set k2 222
QUEUED
127.0.0.1:6379> set k3 333
QUEUED
那此时我们再开启另外一个客户端 , 是查询不到数据的
# 此时在另外一个客户端是查询不到数据的
# 这也就说明我们的数据目前还未被执行
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> mget k1 k2 k3
1) (nil)
2) (nil)
3) (nil)
那我们客户端 1 执行了 exec 操作 , 也就是执行事务操作
127.0.0.1:6379> exec # 执行事务
# 返回三个操作的执行结果
1) OK
2) OK
3) OK
我们也演示一下
那我们还可以取消事务 , 使用 discard 命令
127.0.0.1:6379> multi # 重新开启一个事物
OK
# 修改之前的键值对的值
127.0.0.1:6379> set k1 aaa
QUEUED
127.0.0.1:6379> set k2 bbb
QUEUED
127.0.0.1:6379> set k3 ccc
QUEUED
此时我们 k1 k2 k3 的值仍然是 111 222 333
127.0.0.1:6379> mget k1 k2 k3
1) "111"
2) "222"
3) "333"
那我们执行 discard 命令来去取消当前事务
127.0.0.1:6379> discard # 取消本次事务
OK
然后再去观察 k1 k2 k3 的值 , 依然没有任何问题
127.0.0.1:6379> mget k1 k2 k3
1) "111"
2) "222"
3) "333"
我们也演示一下
❓ 那假如说我们比较点背 , 当我们开启事务并且已经添加若干个指令之后 , 此时服务器重启 , 那这个事务怎样处理呢 ?
✔️ 那很遗憾了 , 此时的效果就相当于 discard 了
我们也能看到 , 当服务器重启之后想要执行事务 , 但是得到的回复却是 (error) ERR EXEC without MULTI , 意思是没有对应的 multi , 这也就说明了之前开启的事务已经失效了 . 而且再去获取 k1 k2 k3 的值 , 也没有发生变化 , 这也印证了事务执行失败 .
那还有一个命令 , 叫做 watch . 他可以监控某个 key 是否在事务的执行前后发生了改变
比如我们现在有两个 Redis 客户端 , 分别修改同一个键值对
那大家觉得最后 key 的值是 222 还是 333 呢 ?
按理说我们客户端 2 修改操作执行的晚 , 所以最后的结果应该就是 333 了 , 那我们实际运行一下看一下结果
实际结果是 222
表面上来看 , 我们先执行的 set key 222 , 后执行的 set key 333
但是实际上 , 我们的 set key 222 是推迟执行的 , 也就是说只有执行了 exec 才算执行了 set key 222 , 那还是 set key 222 更晚一些
那这样就容易产生歧义 , 所以我们可以使用 watch 命令来去监控这个 key , 看看这个 key 在事物的 multi 和 exec 之间 , 是否被其他客户端修改了
我们可以看到 , 在执行了 exec 之后 , 返回值为 nil , 代表事务执行失败 , 这是因为我们 watch 的缘故 , 让第二个客户端修改成功了
三 . watch 的实现原理
watch 的实现 , 类似于一个 “乐观锁” , 通过版本号这样的机制来实现乐观锁
乐观锁和悲观锁并不是一个具体的锁 , 而是一类锁的特征
乐观锁 : 加锁之前就会预测接下来锁冲突的概率比较低
悲观锁 : 加锁之前就会预测接下来锁冲突的概率比较高
锁冲突概率的高低 , 和接下来要做的工作是不一样的
那到此为止 , Redis 中的事务就已经介绍完毕 , 如果对你有帮助的话 , 还请一键三连~~~