阅读 5

redis

一、Redis 的通讯协议是什么

Redis 的通讯协议是文本协议,是的,Redis 服务器与客户端通过 RESP(Redis Serialization Protocol)协议通信。

没错,文本协议确实是会浪费流量,不过它的优点在于直观,非常的简单,解析性能极其的好,我们不需要一个特殊的 Redis 客户端仅靠 Telnet 或者是文本流就可以跟 Redis 进行通讯。

客户端的命令格式:

  • 简单字符串 Simple Strings,以 "+"加号开头。
  • 错误 Errors,以"-"减号开头。
  • 整数型 Integer,以 ":" 冒号开头。
  • 大字符串类型 Bulk Strings,以 "$"美元符号开头。
  • 数组类型 Arrays,以 "*"星号开头。

set hello abc

一个简单的文本流就可以是redis的客户端

简单总结:具体可以见:

redis.io/topics/prot…

Redis 文档认为简单的实现,快速的解析,直观理解是采用 RESP 文本协议最重要的地方,有可能文本协议会造成一定量的流量浪费,但却在性能上和操作上快速简单,这中间也是一个权衡和协调的过程。

二、Redis 究竟有没有 ACID 事务

要弄清楚 Redis 有没有事务,其实很简单,上 Rredis 的官网查看文档,发现:

Redis 确实是有事务,不过按照传统的事务定义 ACID 来看,Redis 是不是都具备了 ACID 的特性。

ACID 指的是:

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

我们将使用以上 Redis 事务的命令来检验是否 Redis 都具备了 ACID 的各个特征。

1、原子性

事务具备原子性指的是,数据库将事务中多个操作当作一个整体来执行,服务要么执行事务中所有的操作,要么一个操作也不会执行。

①事务队列

首先弄清楚 Redis 开始事务 multi 命令后,Redis 会为这个事务生成一个队列,每次操作的命令都会按照顺序插入到这个队列中。

这个队列里面的命令不会被马上执行,直到 exec 命令提交事务,所有队列里面的命令会被一次性,并且排他的进行执行。

对应如下图:

从上面的例子可以看出,当执行一个成功的事务,事务里面的命令都是按照队列里面顺序的并且排他的执行。

但原子性又一个特点就是要么全部成功,要么全部失败,也就是我们传统 DB 里面说的回滚。

当我们执行一个失败的事务

可以发现,就算中间出现了失败,set abc x 这个操作也已经被执行了,并没有进行回滚,从严格的意义上来说 Redis 并不具备原子性。

②为何 Redis 不支持回滚

这个其实跟 Redis 的定位和设计有关系,先看看为何我们的 MySQL 可以支持回滚,这个还是跟写 Log 有关系,Redis 是完成操作之后才会进行 AOF 日志记录,AOF 日志的定位只是记录操作的指令记录。

而 MySQL 有完善的 Redolog,并且是在事务进行 Commit 之前就会写完成 Redolog,Binlog:

要知道 MySQL 为了能进行回滚是花了不少的代价,Redis 应用的场景更多是对抗高并发具备高性能,所以 Redis 选择更简单,更快速无回滚的方式处理事务也是符合场景。

2、一致性

事务具备一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否成功,数据库也应该是一致的。

从 Redis 来说可以从 2 个层面看,一个是执行错误是否有确保一致性,另一个是宕机时,Redis 是否有确保一致性的机制。

①执行错误是否有确保一致性

依然去执行一个错误的事务,在事务执行的过程中会识别出来并进行错误处理,这些错误并不会对数据库作出修改,也不会对事务的一致性产生影响。

②宕机对一致性的影响

暂不考虑分布式高可用的 Redis 解决方案,先从单机看宕机恢复是否能满意数据完整性约束。

无论是 RDB 还是 AOF 持久化方案,可以使用 RDB 文件或 AOF 文件进行恢复数据,从而将数据库还原到一个一致的状态。

③再议一致性

上面执行错误和宕机对一致性的影响的观点摘自黄健宏 《Redis 设计与实现》。

当在读这章的时候还是有一些存疑的点,归根到底 Redis 并非关系型数据库。

如果仅仅就 ACID 的表述上来说,一致性就是从 A 状态经过事务到达 B 状态没有破坏各种约束性,仅就 Redis 而言不谈实现的业务,那显然就是满意一致性。

但如果加上业务去谈一致性,例如,A 转账给 B,A 减少 10 块钱,B 增加 10 块钱,因为 Redis 并不具备回滚,也就不具备传统意义上的原子性,所以 Redis 也应该不具备传统的一致性。

其实,这里只是简单讨论下 Redis 在传统 ACID 上的概念怎么进行对接,或许,有可能是我想多了,用传统关系型数据库的 ACID 去审核 Redis 是没有意义的,Redis 本来就没有意愿去实现 ACID 的事务。

3、隔离性

隔离性指的是,数据库中有多个事务并发的执行,各个事务之间不会相互影响,并且在并发状态下执行的事务和串行执行的事务产生的结果是完全相同的。

Redis 因为是单线程操作,所以在隔离性上有天生的隔离机制,当 Redis 执行事务时,Redis 的服务端保证在执行事务期间不会对事务进行中断,所以,Redis 事务总是以串行的方式运行,事务也具备隔离性。

4、持久性

事务的持久性指的是,当一个事务执行完毕,执行这个事务所得到的结果被保存在持久化的存储中,即使服务器在事务执行完成后停机了,执行的事务的结果也不会被丢失。

Redis 是否具备持久化,这个取决于 Redis 的持久化模式:

  • 纯内存运行,不具备持久化,服务一旦停机,所有数据将丢失。
  • RDB 模式,取决于 RDB 策略,只有在满足策略才会执行 Bgsave,异步执行并不能保证 Redis 具备持久化。
  • AOF 模式,只有将 appendfsync 设置为 always,程序才会在执行命令同步保存到磁盘,这个模式下,Redis 具备持久化。(将 appendfsync 设置为 always,只是在理论上持久化可行,但一般不会这么操作)

简单总结:

  • Redis 具备了一定的原子性,但不支持回滚。
  • Redis 不具备 ACID 中一致性的概念。(或者说 Redis 在设计时就无视这点)
  • Redis 具备隔离性。
  • Redis 通过一定策略可以保证持久性。

Redis 和 ACID 纯属站在使用者的角度去思想,Redis 设计更多的是追求简单与高性能,不会受制于传统 ACID 的束缚。

三、Redis 的乐观锁 Watch 是怎么实现的

当我们一提到乐观锁就会想起 CAS(Compare And Set),CAS 操作包含三个操作数:

  • 内存位置的值(V)
  • 预期原值(A)
  • 新值(B)

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。

在 Redis 的事务中使用 Watch 实现,Watch 会在事务开始之前盯住 1 个或多个关键变量。

当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时, Redis 会检查关键变量自 Watch 之后,是否被修改了。

①Java 的 AtomicXXX 的乐观锁机制

在 Java 中我们也经常的使用到一些乐观锁的参数,例如 AtomicXXX,这些机制的背后是怎么去实现的,是否 Redis 也跟 Java 的 CAS 实现机制一样?

先来看看 Java 的 Atomic 类,我们追一下源码,可以看到它的背后其实是 Unsafe_CompareAndSwapObject:

可以看见 compareAndSwapObject 是 Native 方法,需要在继续追查,可以下载源码或打开 :hg.openjdk.java.net/jdk8u/。

②Cmpxchg

可以发现追查到最终 CAS,“比较并修改”,本来是两个语意,但是最终确实一条 CPU 指令 Cmpxchg 完成。

Cmpxchg 是一条 CPU 指令的命令而不是多条 CPU 指令,所以它不会被多线程的调度所打断,所以能够保证 CAS 的操作是一个原子操作。

当然 Cmpxchg 的机制其实存在 ABA 还有多次重试的问题,这个不在这里讨论。

③Redis 的 Watch 机制

Redis 的 Watch 也是使用 Cmpxchg 吗,两者存在相似之处在用法上也有一些不同,Redis 的 Watch 不存在 ABA 问题,也没有多次重试机制,其中有一个重大的不同是:Redis 事务执行其实是串行的。

简单追一下源码:摘录出来的源码可能有些凌乱,不过可以简单总结出来数据结构图和简单的流程图,之后再看源码就会清晰很多。

存储如下图:

RedisDb 存放了一个 watched_keys 的 Dcit 结构,每个被 Watch 的 Key 的值是一个链表结构,存放的是一组 Redis 客户端标志。

流程如下图:

每一次 Watch,Multi,Exec 时都会去查询这个 watched_keys 结构进行判断,每次 Touch 到被 Watch 的 Key 时都会标志为 CLIENT_DIRTY_CAS。

因为在 Redis 中所有的事务都是串行的,假设有客户端 A 和客户端 B 都 Watch 同一个 Key。

当客户端 A 进行 Touch 修改或者 A 率先执行完,会把客户端 A 从这个 watched_keys 的这个 Key 的列表删除,然后把这个列表所有的客户端都设置成 CLIENT_DIRTY_CAS。

当后面的客户端 B 开始执行时,判断到自己的状态是 CLIENT_DIRTY_CAS,便 discardTransaction 终止事务。

简单总结:Cmpxchg 的实现主要是利用了 CPU 指令,看似两个操作使用一条 CPU 指令完成,所以不会被多线程进行打断。

而 Redis 的 Watch 机制,更多是利用了 Redis 本身单线程的机制,采用了 watched_keys 的数据结构和串行流程实现了乐观锁机制。

四、Redis 是如何持久化的

Redis 的持久化有两种机制,一个是 RDB,也就是快照,快照就是一次全量的备份,会把所有 Redis 的内存数据进行二进制的序列化存储到磁盘。

另一种是 AOF 日志,AOF 日志记录的是数据操作修改的指令记录日志,可以类比 MySQL 的 Binlog,AOF 日期随着时间的推移只会无限增量。

在对 Redis 进行恢复时,RDB 快照直接读取磁盘即可恢复,而 AOF 需要对所有的操作指令进行重放进行恢复,这个过程有可能非常漫长。

1、RDB

Redis 在进行 RDB 的快照生成有两种方法,一种是 Save,由于 Redis 是单进程单线程,直接使用 Save,Redis 会进行一个庞大的文件 IO 操作。

由于单进程单线程势必会阻塞线上的业务,一般的话不会直接采用 Save,而是采用 Bgsave,之前一直说 Redis 是单进程单线程,其实不然。

在使用 Bgsave 的时候,Redis 会 Fork 一个子进程,快照的持久化就交给子进程去处理,而父进程继续处理线上业务的请求。

①Fork 机制

想要弄清楚 RDB 快照的生成原理就必须弄清楚 Fork 机制,Fork 机制是 Linux 操作系统的一个进程机制。

当父进程 Fork 出来一个子进程,子进程和父进程拥有共同的内存数据结构,子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。

一开始两个进程都具备了相同的内存段,子进程在做数据持久化时,不会去修改现在的内存数据,而是会采用 COW(Copy On Write)的方式将数据段页面进行分离。

当父进程修改了某一个数据段时,被共享的页面就会复制一份分离出来,然后父进程再在新的数据段进行修改。

②分裂

这个过程也成为分裂的过程,本来父子进程都指向很多相同的内存块,但是如果父进程对其中某个内存块进行该修改,就会将其复制出来,进行分裂再在新的内存块上面进行修改。

因为子进程在 Fork 的时候就可以固定内存,这个时间点的数据将不会产生变化。

所以我们可以安心的产生快照不用担心快照的内容受到父进程业务请求的影响。

另外可以想象,如果在 Bgsave 的过程中,Redis 没有任何操作,父进程没有接收到任何业务请求也没有任何的背后例如过期移除等操作,父进程和子进程将会使用相同的内存块。

2、AOF

AOF 是 Redis 操作指令的日志存储,类同于 MySQL 的 Binlog,假设 AOF 从 Redis 创建以来就一直执行,那么 AOF 就记录了所有的 Redis 指令的记录。

如果要恢复 Redis,可以对 AOF 进行指令重放,便可修复整个 Redis 实例。

不过 AOF 日志也有两个比较大的问题:

  • 一个是 AOF 的日志会随着时间递增,如果一个数据量大运行的时间久,AOF 日志量将变得异常庞大。
  • 另一个问题是 AOF 在做数据恢复时,由于重放的量非常庞大,恢复的时间将会非常的长。

AOF 写操作是在 Redis 处理完业务逻辑之后,按照一定的策略才会进行些 AOF 日志存盘,这点跟 MySQL 的 Redolog 和 Binlog 有很大的不同。

也因为此原因,Redis 因为处理逻辑在前而记录操作日志在后,也是导致 Redis 无法进行回滚的一个原因。

bgrewriteaof:针对上述的问题,Redis 在 2.4 之后也使用了 bgrewriteaof 对 AOF 日志进行瘦身。

bgrewriteaof 命令用于异步执行一个 AOF 文件重写操作。重写会创建一个当前 AOF 文件的体积优化版本。

3、RDB 和 AOF 混合搭配模式

在对 Redis 进行恢复的时候,如果我们采用了 RDB 的方式,因为 Bgsave 的策略,可能会导致我们丢失大量的数据。

如果我们采用了 AOF 的模式,通过 AOF 操作日志重放恢复,重放 AOF 日志比 RDB 要长久很多。

Redis 4.0 之后,为了解决这个问题,引入了新的持久化模式,混合持久化,将 RDB 的文件和局部增量的 AOF 文件相结合。

RDB 可以使用相隔较长的时间保存策略,AOF 不需要是全量日志,只需要保存前一次 RDB 存储开始到这段时间增量 AOF 日志即可,一般来说,这个日志量是非常小的。

note:困境是成功的首付,你品尝得越多,后面属于你的空间越广阔。

关注下面的标签,发现更多相似文章
评论