2019年2月

Redis 事务

Redis 的事务不同于关系型数据库, 事务模型很不严格, 我们对比着 MySQL 来看

基本用法

命令分为:

  • multi 事务的开始 (类似 MySQL 的 begin)
  • exec 执行事务 (类似 MySQL 的 commit)
  • discard 丢弃事务不执行 (类似 MySQL 的 rollback)
// exec
127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 666
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379> get jw
"666"

这里要注意, 如果 exec返回非 OK (如nil )则表示执行失败

// discard
127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 888
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get jw
(nil)

exec之前的命令都被缓存在事务队列里并为执行, QUEUED是一个简单的字符串, 表示指令已成功缓存到事务队列里.

原子性

文首我们说了 Redis 的事务模型不严格, 主要是指原子性, 请看下面代码

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw_str hehe
QUEUED
127.0.0.1:6379> incr jw_str
QUEUED
127.0.0.1:6379> set jw2 666
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get jw2
"666"

可以看出事务执行到第二个指令失败了(不能对字符串做+1的数学运算), 但是第三个指令仍然被执行. 所以 Redis 事务根本不具备原子性, 只仅仅满足了事务"隔离性"中的串行化--当前执行的事务不被其他事务打断.

watch

考虑一个业务场景, Redis 存储了账户余额, 现在需要对余额增减, 流程是先把值取出来, 修改后再写回去.

这时如果有多个客户端操作这个 key 就会出现并发问题, 而 Redis 提供的 watch 指令就可以解决这个问题., 使用方式如下:

127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> watch jw
OK
127.0.0.1:6379> set jw 666
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 888
QUEUED
127.0.0.1:6379> exec
(nil)

从上面代码示例可以看出, watch 会在事务开始之前就盯住一个变量, Redis 在执行 exec 时会检查变量自 watch 后是否有被修改过, 包括当前客户端在内, 如果被修改过, 则事务执行失败.

注意, Redis 禁止在multiexec 之间执行 watch 指令, 必须在事务开启指令 multi 之前盯住变量

Redis 持久化原理

概述


Redis 是个内存数据库, 数据全部存储在内存中, 如果突然宕机, 数据就会全部丢失, 因此有了将数据刷到硬盘保存的持久化机制. Redis 持久化分两种方式, 一种是 RDB 快照, 另一种是 AOF 日志.

快照就是全量备份, 内存数据的二进制序列化形式, 在存储上非常紧凑. AOF 日志是连续的增量备份, 记录的是内存数据修改的指令记录文本.

快照


由于 Redis 是单线程程序, 还要同时负责多个客户端的并发读写操作和内存数据结构的逻辑读写. 所以快照持久化面临如下两个问题:

  • 快照需要大量 IO 操作可能会阻塞线上业务, 拖慢整体性能, 而且快照又不能使用多路复用
  • 持久化的同时, 还需要接受客户端请求, 正在持久的数据可能会被修改删除

Redis 使用 操作系统的多进程 COW ( Copy on Write ) 机制来实现快照持久化

原理

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程. 子进程做数据持久化, 不会修改现有的内存数据结构, 它只是对现有的数据结构遍历读取, 然后序列化写到磁盘中. 但父进程不一样, 它必须持续服务客户端请求, 然后对内存数据结构进行不间断修改.

这是就会使用到操作系统的 COW 机制来进行数据段页面的分离.

当父进程对一个页面的数据进行修改时, 会将被共享的页面复制一份分离出来, 然后对这个新复制的页面进行修改, 子进程继续复制原有页面, 不会受到影响, 也就是子进程被创建那一刻的数据, 所以为什么叫"快照".

随着父进程的持续修改, 越来越多的共享页面被复制分离出来, 内存就会持续增长, 不用担心内存会暴增, 因为不会超过原有数据内存的2倍. 而且往往冷数据占比较多, 很少会出现所有的页面都被复制分离的情况.

扩展一下, 因为快照要遍历整个内存, 同时大块写磁盘也是非常耗时的IO操作, 所以建议在从节点上进行. 由于从节点可能会因为网络等问题, 造成同步延迟, 就会操作快照不全, 所以监控也很重要.

AOF


aof 日志存储的是 Redis 服务器的顺序指令序列, aof 日志只记录对内存进行修改的记录.

aof 同样也面临着两个问题:

  • 随着实例的运行, 修改性指令序列越来越多, 在存储和重放(重启后恢复)上都存在着性能问题
  • aof 写日志文件的频率, 过快会因为IO拉升机器负载, 过慢如果机器突然宕机就会丢失数据

重写

为解决第一个问题, Redis 提供了 bgrewriteaof 指令用于对 aof 日志瘦身. 其原理是开启一个子进程对现有内存数据进行遍历, 转换成一些列 Redis 操作指令, 序列化到一个新的 aof 日志文件中. 序列化完毕后的再将操作期间发生的增量 aof 日志追加到新的日志中, 然后代替旧的日志文件.

fsync

进程对 aof 日志文件写操作时, 实际是将内容写到操作系统内核为文件描述符分配的一个内存缓存中, 然后内核 异步 将数据刷到磁盘.

为解决第二个问题, Redis 提供配置刷新周期, 通常是 1s 就会调用 Linux 的 glibc 提供的 fsync(int fd)函数, 它可以将指定文件的内容强制从内核缓存刷到磁盘.

扩展一下. Redis 还提供了另外两种可选方案, 生产环境中基本不会使用, 了解即可

  • 一是永不调用 fsync, 让操作系统来决定合适刷到磁盘
  • 另一个是每来一条指令就调一次 fsync

混合持久化


Redis 4.0 提供

快照和aof日志都有各自的痛点

  • 快照因为是每隔一段时间持久化一次, 就会丢失宕机时刻与上一次持久化之间的数据
  • aof 因为存储的是指令序列, 恢复重放时要花费很长时间

综合利弊, 使用 aof 还是更靠谱一点, 现在 Redis 4.0 提供了更好的混合持久化选项

混合持久化

  • 将 rdb 文件的内容和增量的 aof 日志放在一起
  • aof 日志只存储 rdb 持久化开始到当前发生的增量日志
  • 重启时, 先加载 rbd 内容, 再重放增量 aof 日志

这样就可以解决上面的两个痛点