合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
>[info] redis 事务 任何数据库都要有一套自己的事务控制机制,redis事务是一次可以执行多个命令,它的本质是一组命令的 集合。 一个事务中所有的命令都会被序列化,在事务执行的过程中会按照顺序执行队列中的命令。其它客 户端提交的命令请求会等到事务执行完毕再执行。 * 总的来说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 ***** **与其他数据库相比:** 1. redis事务是分为三个阶段:开始事务、命令入队、执行事务。 2. redis事务不具有隔离级别的概念:redis在发送exec命令之前,命令操作只是被放入到队列缓存当中,并 不会被实际执行,因此也就不能类似关系型数据中,在事务内查询已经变更的操作,事务外的客户端更不 能查询到事务内的数据。 3. redis事务是不保证原子性的:redis事务只保证在命令格式只有在都正确的情况下才会都执行,要不就都 不执行命令。但是事务的整体是不保证原子性的,且没有回滚,当事务中任意一个命令执行失败,其余的命令依然会执行。 **redis 事务执行过程:** ![](https://img.kancloud.cn/32/4c/324c385f257bb93ea784516c48b590db_1273x824.png) ![](https://img.kancloud.cn/6e/b9/6eb987507211c77e902d2ca3e8c9e03a_1018x304.png) ***** **事务命令入队过程:** ![](https://img.kancloud.cn/a0/ff/a0ffbb04bf509ad46a5e3c47ae3e7bf9_1135x642.png) ***** **事务ACID:** ![](https://img.kancloud.cn/db/9d/db9d945cb5672c2e16357abfbed8bcb8_1021x663.png) 1. 持久性 事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果巳经被保存到 永久性存储介质 (比如硬盘)里面了, 即使服务器在事务执行完毕 之后停机, 执行事务所得的结果也不会丢失。Redis事 务的耐久性由服务器所使用持久化模式决定的: ``` (1) 当服务器在无持久化的内存模式下运作时,事务不具有耐久性。因为一旦服务器停机, 服务器所有的数据都将丢失。 (2) 当服务器在ROB持久化模式下运作时,事务同样不具有耐久性。因为服务器只会在特定的保存条 件下才会执行BGSAVE命令,并且异步执行的BGSAVE命令不能保证事务的数据第一时间被保存到硬盘 上。 (3) 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命 令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里。 ``` 2. 隔离性 事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相 影响,并且在并发 状态下执行的事务和串行执行的事务产生的结果完全相同。 因为Redis使用单线程的方式来执行事务(以 及事务队列中的命令),并且服务器保证, 在执行事务期间不会对事务进行中断,因此,Redis的事务总是 以串行的方式运行的,并且 事务也总是具有隔离性的。 3. 一致性 事务的一致性是指,如果数据库执行前是一致的,那么在事务执行后,无论事务是否执行成功,数据库也 应该是一致的。 >[] 入队错误 如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况, 那么Redis将拒绝执行这个事务。因为服务器会拒绝执行人队过程中出现错误的事务, 所以Redis事务的一致性不会被带有入 队错误的事务影响。 ``` 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set msg heelo QUEUED 127.0.0.1:6379(TX)> get msg QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) "heelo" 127.0.0.1:6379> ``` **Redis 2.6.5以前的入队错误处理:Redis会忽略错误的命令,而正确的命令如上面的SET和GET仍然 会被执行。** >[] 执行错误 执行错误通常都是一些不能在入队时被服务器发现的错误, 这些错误只会在命令实际执行时被触发。即使 在事务的执行过程中发生了错误, 服务器也不会中断事务的执行, 它会继续执行事务中余下的其他命令, 并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响。 因为在事务执行的过程中, 出错的命令会被服务器识别出来, 并进行相应的错误处理, 所以这些出错命令不会对数据库做任何修改, 也不会对事务的一致性产生任何影响。 ~~~ 127.0.0.1:6379> set msg hello OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> sadd fruit apple banana cherry QUEUED 127.0.0.1:6379(TX)> rpush msg bye redis QUEUED 127.0.0.1:6379(TX)> sadd alphabet a b c QUEUED 127.0.0.1:6379(TX)> exec 1) (integer) 3 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) (integer) 3 127.0.0.1:6379> ~~~ 还有一种情况是与Redis的持久化相关,这里暂时不做解释,在之后的课程会进行补充。 ***** 4. 原子性 事务具有原子性指的是, 数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所 有操作, 要么就一个操作也不执行。 对于Redis的事务功能来说,事务队列中的命令要么就全部都执行, 要么就一个都不执行,因此, Redis的事务是具有原子性的。 下面是一个执行失败的事务,这个事务因为 命令入队出错而被服务器拒绝执行: ***** Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback), 即使事务队 列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执 行完毕为止。 下面展示了即使RPUSH命令在执行期间出现了错误,事务的后续命令也会继续执行下去, 并且之前执行的命令也不会有任何影响: ***** **Redis为什么不支持回滚:** 不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符, 并且Redis事务的执行时错误通常都是编程错误产生的, 这种错误通常只会出现在开发环境中, 而很少会在 实际的生产环境中出现。 ![](https://img.kancloud.cn/ce/4b/ce4bcc1096b8df9abfdcac685502667b_957x184.png) ***** >[info] redis 乐观锁 **事务WATCH命令监控:** Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动, 那么事务将被打断。 ``` 开启第一个客户端 127.0.0.1:6379> watch name OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name starsky QUEUED 127.0.0.1:6379> get name QUEUED 127.0.0.1:6379> 开启第二个客户端 127.0.0.1:6379> watch name OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name harry QUEUED 127.0.0.1:6379> get name QUEUED 127.0.0.1:6379> 客户端一,执行事务 127.0.0.1:6379> exec 1) OK 2) "starsky" 127.0.0.1:6379> 客户端二,执行事务 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> ``` ***** **乐观锁的实现方式:** ![](https://img.kancloud.cn/6c/c1/6cc1dbc1e698fc0f7522b8c0930e2556_1260x299.png) >[] MVCC方式实现 一般是在数据表中加上版本号字段 version,表示数据被修改的次数。当数据被修改时,这个字段值会加1。 ``` 举个简单的例子:假设帐户信息表中有一个 version 字段,当前值为 1 ,而当前帐户的余额( balance )为 100 。 +----+--------+---------+ | id | price | version | +----+--------+---------+ | 1 | 100.00 | 1 | +----+--------+---------+ 1 row in set (0.01 sec) 1.操作员 A 此时准备将其读出( version=1 ),并从其帐户余额中扣除 50( 100-50 ); mysql> select * from mvcc_version where id=1 and version=1; +----+--------+---------+ | id | price | version | +----+--------+---------+ | 1 | 100.00 | 1 | +----+--------+---------+ 1 row in set (0.01 sec) 2.操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20 ( 100-20 ); mysql> select * from mvcc_version where id=1 and version=1; +----+--------+---------+ | id | price | version | +----+--------+---------+ | 1 | 100.00 | 1 | +----+--------+---------+ 1 row in set (0.01 sec) 3.操作员 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50 ),提交到数据库完成更新; mysql> update mvcc_version set price=price-50,version=version+1 where id=1 and version=1; Query OK, 1 row affected (0.00 sec)//1条数据受到影响 Rows matched: 1 Changed: 1 Warnings: 0 4.操作员 B 完成了操作,也将版本号加1( version=2 )试图向数据库提交数据( balance=80 ),但此时比对数据库记录版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本 也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。 mysql> update mvcc_version set price=price-50,version=version+1 where id=1 and version=1; Query OK, 0 rows affected (0.01 sec)//受到影响的条数为0 Rows matched: 0 Changed: 0 Warnings: 0 ``` 因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改,最终造成覆盖 操作员 A 操作结果的可能。 ***** >[] redis 事务 watch 监控 key(模拟秒杀) 事务一: ![](https://img.kancloud.cn/69/93/6993efe7a667afadee84a778235b909c_645x792.png) 事务二: ![](https://img.kancloud.cn/14/57/145747d1cda57a45bb0e7cd75a05383e_477x466.png) ***** >[info] stream 数据类型 Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会 被丢弃。 ***** **关于消息队列:** >[] 什么是消息队列? 把数据放到消息队列叫做:**生产者** 从消息队列里边取数据叫做:**消费者** ![](https://img.kancloud.cn/23/b3/23b364db4ee114b551fd10d2fc55abdd_1145x643.png) 我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发 送消息1,2,3...对于消费者就会按照1,2,3...的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况, 比如某个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保 证消息被消费的顺序正确。 ***** >[info] 为什么使用消息队列? 在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增, 使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息 队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库 (消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。 ***** 通过以上分析我们可以得出消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产 生的事务消息存储在消息队列中,从而削平高峰期的并发事务。 ***** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入 对系统的冲击。 因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等 操作中可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提 交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进 程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就 类似我们平时手机订火车票和电影票。 ***** >[] 缺点 1. 系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消 息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! 2. 系统复杂性提高: 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息 传递的顺序性等等问题! 3. 一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。 但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! ***** **stream数据类型:** Redis5.0中发布的Stream类型,也用来实现典型的 **消息队列**。该Stream类型的出现,几乎满足了消息队列具 备的全部内容,包括但不限于: * 消息ID的序列化生成 * 消息遍历 * 消息的阻塞和非阻塞读取 * 消息的分组消费 * 未完成消息的处理 * 消息队列监控 >[] stream类型的使用 **xadd命令:** 命令用于在某个stream中追加消息 ``` 127.0.0.1:6379> xadd memberMessage * user starsky age 20 "1610349286147-0" 127.0.0.1:6379> xadd memberMessage * user will age 30 "1610349299077-0" 127.0.0.1:6379> ``` 格式: ``` XADD key ID field string [field string ...] 需要提供key,消息ID方案,消息内容,其中消息内容为key-value型数据。 ID,最常使用*,表示由Redis生成消息ID,这也是强烈建议的方案。 field string [field string], 就是当前消息内容,由1个或多个key-value构成。 ``` ***** **xlen命令:** 返回结果为stream数据类型的长度 ``` 127.0.0.1:6379> xlen memberMessage (integer) 2 127.0.0.1:6379> ``` ***** **xrange命令:** 获取消息列表,会自动过滤已经删除的消息 ``` # -表示最小值, +表示最大值 127.0.0.1:6379> xrange memberMessage - + 1) 1) "1610349286147-0" 2) 1) "user" 2) "starsky" 3) "age" 4) "20" 2) 1) "1610349299077-0" 2) 1) "user" 2) "will" 3) "age" 4) "30" ``` ***** **xread命令:** 我们可以在不定义消费组的情况下进行Stream消息的独立消费,当Stream没有新消息时,甚至可以阻塞等 待。Redis设计了一个单独的消费指令xread,可以将Stream当成普通的消息队列(list)来使用。使用xread时, 我们可以完全忽略消费组(Consumer Group)的存在,就好比Stream就是一个普通的列表(list)。 ``` 从Stream头部读取两条消息 127.0.0.1:6379> xread count 1 streams memberMessage 0-0 1) 1) "memberMessage" 2) 1) 1) "1610349286147-0" 2) 1) "user" 2) "starsky" 3) "age" 4) "20" ``` ***** **xgroup create命令:** Stream通过xgroup create指令创建消费组(Consumer Group),需要传递起始消息ID参数用来初始 化last_delivered_id变量。 ``` 127.0.0.1:6379> xgroup create memberMessage starsky 0-0 # 表示从头开始消费 OK 127.0.0.1:6379> xgroup create memberMessage will $ # $表示从尾部开始消费,只接受新消 息,当前Stream消息会全部忽略 OK 127.0.0.1:6379> ``` ***** **xinfo命令:** 获取Stream信息 ``` 127.0.0.1:6379> xinfo stream memberMessage 1) "length" 2) (integer) 2 共2个消息 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "last-generated-id" 8) "1610349299077-0" 9) "groups" 10) (integer) 2 #两个消费组 11) "first-entry" 第一个消息 12) 1) "1610349286147-0" 2) 1) "user" 2) "starsky" 3) "age" 4) "20" 13) "last-entry" 最后一个消息 14) 1) "1610349299077-0" 2) 1) "user" 2) "will" 3) "age" 4) "30" ``` ***** **xreadgroup group命令:** Stream提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处 理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从 PEL中移除。 ``` # >号表示从当前消费组的last_delivered_id后面开始读 # 每当消费者读取一条消息,last_delivered_id变量就会前进 127.0.0.1:6379> xreadgroup GROUP starsky xk count 1 streams memberMessage > 1) 1) "memberMessage" 2) 1) 1) "1610349286147-0" 2) 1) "user" 2) "starsky" 3) "age" 4) "20" 127.0.0.1:6379> xreadgroup GROUP starsky xk count 1 streams memberMessage > 1) 1) "memberMessage" 2) 1) 1) "1610349299077-0" 2) 1) "user" 2) "will" 3) "age" 4) "30" 127.0.0.1:6379> ``` ***** **xack命令:** ``` 127.0.0.1:6379> xack memberMessage starsky 1610349286147-0 (integer) 1 127.0.0.1:6379> ```