Redis事务:简单但实用的打包执行

目录

认识事务

事务相关命令

MULTI

EXEC

DISCARD

WATCH

原理

小结


认识事务

说到redis事务,我们就可以联想到MySQL的事务,但是redis的事务要比MySQL的简单不少

MySQL的事务主要是转账的场景

MySQL事务涉及到:

1.原子性:把多个操作打包成一个整体(要么做,要么不做)

2.一致性:事务执行前和之后,数据都不能离谱

3.持久性:事务中做出的修改都会存在硬盘中(服务器重启,数据依然存在)

4.隔离性:事务并发执行,涉及到的一些问题

对于隔离性,我们需要在这里做一些权衡,要么隔离性提高了,数据靠谱了,效率降低了,因为并发程度降低了;如果隔离性降低了,并发程度提高了,效率提高了,但是数据开始离谱了,在这里我们需要根据实际情况,调整MySQL的隔离级别到符合我们的一个情况

相比于MySQL的事务,redis这里事务就较为朴素了

redis事务:

1.原子性: 对于redis的事务到底有没有原子性这个问题存在着争议

原子性最原本的含义,是把多个操作打包到一起,要么全都执行,要么不执行(不保证成功,如果事务中若干个操作,存在有失败的,那就失败吧,不会有回滚操作)

但是redis做到了上述的含义

只是在这一方面,MySQL走得更远,也是把多个操作打包在一起,要么全都执行成功,要么全都不执行

但是这里的区别就是:MySQL事务中如果有操作执行失败,要进行回滚,把中间已经执行的操作,全都回退了,回到起点

MySQL的标准,提高了"原子性"的门槛,这就使得人们谈到原子性的时候,更多的是想到的MySQL这种带有回滚的原子性

2.不具备一致性:redis没有约束,也没有回滚机制,事务执行过程中如果某个修改操作出现失败,就可能引起不一致的情况

3.不具备持久性:redis本身是内存数据库,数据是存储在内存中的,虽然redis也有持久化机制(rdb、aof),但要明确这个持久化机制只是锦上添花,redis以内存为主体,开启持久化机制与否,使用rdb还是aof,与事务没有什么直接关系

4.不涉及隔离性:redis是一个单线程模型的服务器程序,所有的请求/事务,都是"串行"执行的(一个执行完才执行下一个),并行才需要隔离

但是存在既有存在的道理

redis的事务,主要的意义,就是为了"打包",避免其他客户端命令,插队插到中间

例子:

你和朋友去吃烧烤,你先到,朋友后到,你先点了一部分烧烤,让老板等你朋友来了再烤,等你朋友来的时候,你朋友又加了一部分烧烤,然后你们让老板一起烤

这里的不被插队,不是先抢占位置,而是先让出位置,等到需要统一执行的时候,再一起执行

redis中实现事务,是引入了队列(每个客户端都有一个)

开启事务的时候,此时客户端输入的命令,就会发给服务器并且进入这个队列中(而不是立即执行)

当遇到了"执行事务"命令的时候,此时就会把队列中的这些任务都按照顺序依次执行(redis主线程中完成的),主线程会把事务中的操作都执行完,再处理别的客户端

Redis的事务为啥弄得这么简单,为啥不设计成和MySQL一样强大呢?

MySQL的事务,背后是要付出很大的代价的:

空间上,要花费更多的空间来存储更多的数据

时间上,也要有更大的执行开销

正因为这些问题,才有了Redis

啥时候需要用到redis的事务呢?

如果我们需要把多个操作打包进行,使用事务是比较合适的(商品秒杀)

还有一个概念(超卖):比如放货5000台,实际如果让5001个人下单成功,就是超卖

这里就需要加上限制:

不加上任何限制,就会有"线程安全"问题

如果存在两个客户端同时运行,都同时获取到为1,可以下单,依旧是超卖问题

以前是在多线程中,通过加锁的方式来避免"插队"问题,这里再redis中直接使用事务即可

以下是两个客户端对应要进行的操作

执行这些操作的时候,我们就会发现,中间代码部分会统一发给服务器,服务器就会在队列记录下这些操作,但这些操作不会立即执行(只是排队),而是等到redis服务器收到执行事务的时候才会真正执行

第一个客户端做完这些之后,第二个客户端也开始发送这些操作,同样也是收到执行事务的时候也会真正执行

第二个客户端的"执行事务"命令发过来之后,服务器才真正执行第二个事务中的内容,此时第一个事务执行事务命令已经运行过了,此时,第二个事务get到的count就已经是第一个事务自减之后的结果了

这个场景中,没有加锁,也能解决超卖问题

通过上面的场景,也看得出来,确实redis的事务的应用场景,没有MySQL的事务那么多

redis如果按照集群模式进行部署,是不支持事务的

redis支持lua脚本,通过lua脚本就可以实现上述的条件判定,并且也和事务是一样是打包批量执行的

lua脚本的实现方式是redis事务的进阶版本

事务相关命令

开启事务:

MULTI

开启之后,后续再给服务器发送的操作,都会进入那个事务队列里面

执行事务:

EXEC

告诉redis服务器之前入队列的这些操作进行统一执行

放弃当前事务:

DISCARD

把当前队列中的这些命令全部丢弃掉

在服务器的事务队列中,保存了上述请求

此时如果另外开一个客户端,再尝试查询这几个key对应的数据,是没有结果的

只有执行了EXEC命令,才会执行这些操作

如果我在这里重新设置几个数值,在另外一个客户端中查询到的还是旧的值

这个时候使用discard,就相当于上述的操作没有生效

当开启事务,并且给服务器发送若干个命令之后,此时服务器重启,此时的效果就相当于discard

WATCH

监控某个key是否在事务执行之前发生了改变

在上面这个场景中,从时间上来看,客户端1是先发送了set key 222,客户端2是后发送了set key 333,由于客户端1中,得是exec执行了,才会真正执行set key 222,这个操作变成了实际更晚执行的操作,最终值是222

这个场景就可以使用watch命令来进行监控这个key

看看这个key在事务的multi和exec之间,set key之后,是否在外部被其他客户端修改了

此时,exec在执行上述事务中的命令的时候,发现key在外部有修改,于是在真正执行set key 222的时候就没有真正执行

原理

watch的实现,类似于一种"乐观锁"

乐观锁、悲观锁:不是指某个具体的锁,而是指的是某一类锁的特性

乐观锁:加锁之前,就有一个心理预期,预期接下来锁冲突的概率比较低

悲观锁:枷锁之前,也有一个心理预期,接下来锁冲突(两个线程对同一个锁加锁,一个能加锁成功,另一个阻塞等待)的概率比较高

锁冲突概率高,和锁冲突概率低,接下来要做的工作是不一样的(电脑录播的锁)

redis的watch就相当于是基于版本号这样的机制,来实现了"乐观锁"

当执行watch key的时候,就会给这个key安排一个版本号,版本号可以理解成一个"整数",每次在修改的时候,版本号都会"变大"

watch本质是给exec加了个判定条件

只要是对此处key做出了修改,就会引起版本号变大

exec:在执行事务中的命令的时候,此处就会做出判定,判定当前这个key的版本号,和最初watch的时候记录的版本号是否一致(老王看枕头判断是否出轨)

如果一致,说明当前key在事务开启到最终执行这个过程中,没有别的客户端修改,于是才能真正进行设置

如果不一致,说明key在其他客户端中改过了,因此此处就直接丢弃事务中的操作,exec返回nil

小结

redis的事务要比MySQL的事务简单得多

1.原子性:redis的事务,并不支持回滚

2.一致性:redis并不会保证事务执行前和执行后,内容统一

3.持久性:redis主要通过内存来存储数据

4.隔离性:redis自身作为一个单线程的服务器模型,上面处理的请求本质上都是串行执行的

redis中的lua脚本,也能起到类似于事务的效果,官方网站上说,事务这里的任何能实现的效果,都可以用lua脚本代替

相关推荐
极客小云9 小时前
【用 Go 写一个统一的 LLM Token 统计库:tokencalc 的设计与实现】
开发语言·后端·golang
GISer_Jing9 小时前
后端系统稳定性基石:数据库设计、接口幂等性与边界case处理全链路实战
数据库·oracle·架构
无忧.芙桃9 小时前
MySQL安装与基础操作指南
数据库
Vect__9 小时前
C++转go的之路:变量声明、iota、函数、切片、init、defer
开发语言·后端·golang
fengxin_rou9 小时前
【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
spring boot·后端·elasticsearch
还是鼠鼠10 小时前
AI掘金头条新闻系统 (Toutiao News)-用户注册-生成Token
后端·python·mysql·fastapi·web
自珍JAVA16 小时前
访问者模式:让你的代码优雅地“拜访”对象结构
后端
ZFSS17 小时前
Localization Translate API 集成与使用指南
java·服务器·数据库·人工智能·mysql·ai编程
东风破13719 小时前
达梦DMDRS搭建、以及DMDRS双向同步
数据库·oracle·dm达梦数据库