尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Redis事务机制

Redis事务机制 文章目录事务的概念一、redis事务的特性1.1 原子性1.2 一致性1.3 持久性1.4 隔离性二、事务的意义与实现原理三、事务的操作命令3.1 multi、exec、discard3.2 watch与unwatch3.3 watch的实现原理四、应用场景事务的概念说到事务最经典的例子就是mysql中的转账操作A给B转账500元需要先把A的余额减500再把B的余额加500。这两个操作必须打包成一个整体要么全都执行要么一个都不执行绝不能只执行一半——否则钱就凭空消失了。mysql的事务有四个核心特性也就是常说的ACID原子性Atomicity把多个操作打包成一个整体要么全部成功要么全部失败。一致性Consistency事务执行前和执行后数据都需要和预期保持一致。持久性Durability事务中做出的修改都会存储到硬盘上。隔离性Isolation多个事务并发执行时会引发一系列问题通过隔离级别来控制。注隔离性属于事务的一种性质或者说管理机制mysql还借助MVCC等机制来实现更高效的并发控制。redis也支持事务但和mysql相比更像是一个半成品实现得相当简单。下面就逐条对照ACID来看看redis的事务到底缺了什么。一、redis事务的特性1.1 原子性redis的事务是否具备原子性这件事其实是存在争议的。原子性原本的含义是把多个操作打包在一起要么全执行要么全不执行。而mysql把这个门槛提高了mysql的事务要么执行成功要么全部不执行一旦中途失败还能通过回滚恢复到事务开始前的状态。redis则不保证这一点事务中的某个命令执行失败了就是失败了前面已经执行成功的命令并不会被撤销也没有任何回滚机制。正因为大家一提到原子性就习惯性地对标mysql所以就出现了两种声音有人认为redis有原子性它确实做到了打包执行有人认为没有它做不到失败回滚。有意思的是redis旧版本的官方文档里明确写着事务具有原子性而新版本的文档中这句话又被去掉了。1.2 一致性redis没有像mysql那样的约束主键约束、外键约束、非空约束等也没有回滚机制事务失败了就是失败了因此完全有可能出现数据不一致的情况。1.3 持久性redis虽然支持持久化机制RDB和AOF但那是redis整体的能力和事务本身没有关系。事务并不保证自己所做的修改一定落到了硬盘上所以从事务的角度讲redis是不具备持久性的。1.4 隔离性这一条redis压根就不涉及。redis是单线程处理请求的程序所有命令都是串行执行的天然不存在多个事务并发交叉执行的问题自然也就不需要隔离级别这套东西。可以看出ACID四条里redis基本只沾了原子性的一点边。那redis的事务到底还有什么用呢二、事务的意义与实现原理redis事务的核心意义只有一个把一个业务需要的多个命令打包在一起执行避免其他客户端的命令插队到中间。它的实现方式也很朴素——引入了一个队列每个客户端各有一个且只有在开启事务时才会分配。开启事务之后客户端发送的命令并不会立即执行而是先进入这个队列排队直到遇到执行事务这个命令时才把队列中的所有任务按顺序依次执行完。那redis的事务为什么做得这么简单不设计成mysql那样强大呢因为mysql事务的强大是付出了代价的空间上要额外维护回滚日志、版本链等结构时间上也有更大的开销。正是mysql存在这些重的问题才给了redis这种轻量方案上场的机会。用不用得上取决于业务对可靠性和性能的取舍。三、事务的操作命令3.1 multi、exec、discardmulti功能开启一个事务。语法multi执行之后当前客户端就进入了事务状态后续的命令都会被放进队列而不是立即执行。exec功能执行事务把队列中排队的命令按顺序全部执行掉。语法exec返回值一个数组依次对应队列中每个命令的执行结果。可以看下面的效果在multi之后输入的命令返回的都是QUEUED说明它们只是被保存到队列里了并没有真正执行。此时另开一个客户端去查询也确实查不到这些数据只有exec之后才会真正生效。discard功能丢弃事务把队列中攒下的命令全部清空一个都不执行。语法discard验证discard的效果那么如果开启事务并且给服务器发了若干命令之后服务器重启了会怎么样因为队列是保存在内存中的重启之后这些排队的命令自然全都没了效果就等价于执行了一次discard。3.2 watch与unwatch前面提到事务中的命令是在exec的时候才真正执行的也就是说命令的实际执行时机被推后了。这就容易产生歧义在multi到exec这段时间里如果有其他客户端把我关心的key改掉了我这个事务再执行下去可能就不符合预期了。为了解决这个问题redis提供了watch。watch功能监控一个或多个key观察它们在事务执行前是否被改动过。语法watch key [key ......]它的作用是检查这些key在multi和exec之间是否被外部其他客户端修改过。如果被修改了那么exec时整个事务就不会真正执行直接返回nil。使用watch注意watch必须搭配事务使用并且必须在multi之前执行否则是没有意义的。unwatch功能取消对所有key的监控是watch的反向操作。语法unwatch用法和watch类似这里不再赘述。3.3 watch的实现原理watch本质上是一个乐观锁。需要说明的是乐观锁和悲观锁并不是指某种具体的锁而是指锁的一种特性或者说思路乐观锁预期锁冲突的概率很低所以先不加锁等真正要提交的时候再检查有没有冲突。悲观锁预期锁冲突的概率很高所以干脆一上来就把资源锁住不让别人碰。redis的具体做法是给被watch的key分配一个版本号每次这个key被修改版本号都会变大。等到执行事务时再判断当前key的版本号和最初watch时记录下来的版本号是否一致一致就说明期间没有其他客户端修改过事务正常执行不一致就说明数据已经变了事务直接放弃。这个思路和CASCompare And Swap中通过版本号解决ABA问题的做法是非常类似的。四、应用场景那么什么时候会需要用到redis的事务呢最典型的就是秒杀这类场景。假设要卖一批限量商品一个很自然的写法是//获取仓库中的商品个数intcountredis 执行命令get goods:count;if(count0){//下单成功下单();//商品数量减一redis 执行命令decr goods:count;}这段代码存在明显的线程安全问题假如库存只剩1件两个客户端几乎同时读到count 1于是都判断为可以下单最后都执行了decr库存变成了-1也就是出现了超卖。问题的根源在于判断和扣减这两个操作不是原子的中间被其他请求插队了。在多线程编程中我们会用加锁来避免这种插队而在redis中用事务就能达到同样的效果。把这组命令放到事务里之后第一个事务的命令会先完整执行完等第二个事务执行时读到的库存就已经是扣减后的结果了从而避免了超卖。需要注意的两点redis按集群模式部署时事务的能力会受到很大限制事务中涉及的所有key必须落在同一个节点同一个slot上跨节点的多个key没办法放在一个事务里执行。除了事务redis还支持lua脚本。lua脚本同样能保证一组操作被打包原子地执行而且还能在脚本里完成条件判断、循环等复杂逻辑比事务灵活得多。上面那段先判断再扣减的逻辑用lua脚本实现会更自然实际开发中也更常见。非常感谢您能耐心读完这篇文章。倘若您从中有所收获还望多多支持呀
返回列表