Redis Concurrency

Redis原理

1. Redis支持发布订阅模式
2. Redis事务
    1. Redis的单个命令是原子性的,要么成功要么失败,不存在并发干扰问题。如果涉及多个命令,需要把多个命令作为一个不可分割的处理序列,Redis提供事务的功能:
      1. 按进入队列的顺序进行
      2. 不会收到其他客户端请求的影响
      3. 事务不能嵌套,多个multi命令效果一样
    1. Redis的事务涉及四个命令,multi(开启事务)、exec(执行事务)、discard(取消事务)、watch(监视)
      1. 通过multi命令开启事务。multi执行后,客户端可以继续向服务器发送任意多条命令,不会立即执行而是被放到一个队列。
      2. 执行exec时,所有队列中命令才会被执行;如果没有执行exec,所有命令都不会被执行
      3. discard可以清空事务队列,放弃执行
      4. watch命令,防止事务过程中某个key的值被其他客户端请求修改,带来非预期的结果。即多个客户端更新变量的时候,会跟原值比较,只有他没有被其他线程修改的情况下才更新成新值。
        1. 我们可以用watch监视一个或多个key,如果开启事务后,至少有一个被监视的key键在exec执行之前被修改了,那么整个事务都会被取消
      1. 事务可能遇到的问题
        1. 在执行exce之前发生错误,例如语法错误,包括参数名,参数数量(编译错误)等。事务会被拒绝执行,队列中所有命令都不会执行。
        2. 在执行exce之后发生错误,例如数据类型错误(运行时错误)等。只有错误的命令没有被执行,但是其他命令没有被影响
        3. 我们没发用Redis的这种事务机制来实现原子性,保证数据一致性。
3. Lua脚本
    1. 使用Lua脚本来执行Redis命令为好处:
      1. 一次发送多个命令,减少网络开销
      2. Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性
      3. 对于复杂的组合命令,我们可以放在文件中,实现命令复用
    1. Lua使用技巧:
      1. 缓存Lua脚本。在脚本比较长的情况下,每次调用脚本都需要把整个脚本都传给Redis服务,会产生较大的网络开销。解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以通过摘要码来执行Lua脚本。两个命令:
        1. script load “return ‘Hello World’”
        2. evalsha “摘要码”
      1. 脚本超时。脚本执行有一个超时时间,默认为5秒钟。超过5秒钟,其他客户端命令不会等待,直接返回“BUSY”错误。这样也不行,不能一直拒绝其他客户端命令执行,提供两个命令:
        1. script kill,终止脚本的执行,但是遇见set、del命令,会返回UNKILLABLE错误。原因是为了保证脚本运行的原子性。
        2. 遇见上面的情况只能通过shutdown nosave 命令,直接把Redis服务停掉。
4. Redis为什么这么快
    1. Redis一般情况下支持并发大概10万QPS左右
    2. Redis快主要包括以下3点:
      1. 纯内存结构
        1. KV结构的内存数据库,时间复杂度O(1)。
      1. 请求处理单线程,处理客户端请求是单线程的,这样的好处:
        1. 没有创建线程,销毁线程带来的消耗
        2. 避免了上下文切换导致的CPU消耗
        3. 避免线程之间带来的竞争问题,例如加锁释放锁等等
      1. I/O多路复用机制
        1. 基本原理就是不再是由应用程序自己监控连接,而是由内核替应用程序监视文件描述符
        2. 常见的多路复用器.
5. 内存回收
    1. 过期策略
      1. 立即过期(主动淘汰),每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期数据,对内存友好;但是会占用大量的CPU资源去处理过期数据,从而会影响缓存的响应时间和吞吐量
      2. 惰性过期(被动淘汰),只有访问一个key时,才会判断该key是否过期,过期则清除。该策略可以最大化地节省CPU资源,对内存非常不友好。
      3. 定期过期,每隔一段时间会扫描一定数据库的expires字典中的一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。
      4. 总结:Redis中同时使用了惰性过期和定期过期两种过期策略,并不是实时地清除过期key
    1. 淘汰策略
      1. 前提:Redis内存淘汰策略是指,内存使用达到最大的内存极限时,需要使用淘汰算法来清理掉哪些数据,以保证新数据的存入。
      2. 最大内存设置:maxmemory,如果不设置或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存
      3. 淘汰策略,大概分为几种策略
        1. LRU,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰
        2. LFU,最不常用,按照使用频率删除
        3. random,随机删除
        4. 总结:noeviction默认策略,不删除任何数据,决绝所有写入操作比返回OOM
        5. LRU原理:略
        6. LFU原理:略
6. 持久化机制
    1. Redis提供两种持久化方案,RDB快照和AOF。持久化是Redis和Memcache的主要区别之一
    2. RDB,是Redis默认的持久化方案(如果开启了AOF,优先使用AOF)。当满足一定的条件,会把内存中的数据写入磁盘,生成一个快照文件dump.rdb。
      1. RDB触发
        1. 自动触发
          1. 配置规则触发,redis.conf,定义了触发把数据保存在磁盘的触发频率
          2. shutdown触发,保证服务器正常关闭
          3. flushall触发备份,单rdb文件是空的
        1. 手动触发
          1. save
          2. bgsave
      1. RDB的优势
        1. RDB是一个非常紧凑(compact)的文件,它保存了redis在某个时间点上的数据集。非常适用于进行备份和灾难恢复
        2. 生成RDB文件时,Redis主进程会fork一个子进程来处理所有保存工作,主进程不需要进行任何磁盘工作
        3. RDB在恢复大数据集时的速度比AOF的更快
      1. RDB的劣势
        1. 没办法做到实时持久化。因为bgsave每次运行都要执行fork创建子进程,执行成本过高
        2. 在一定间隔时间做一次备份,如果redis意外down掉,就会丢失最后一次快照之后的所有修改
    1. AOF(Append Only File)
      1. 默认不开启。AOF采用日志的形式来记录每个操作,并追加到文件中。只要执行Redis命令,就会把命令写入到AOF文件中
      2. 数据都是实时持久化到磁盘的吗?
        1. 由于操作系统的缓存机制,AOF数据并没有实时地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?
      1. 文件越来越大怎么办?
        1. 随着AOF的文件越来越大,文件越大,占用服务器内存越来越大以及AOF恢复时间越长。可能会出现一个问题:记录的命令很多都是重复的,有效的结果很少。为了解决这个问题,Redis增加了重写机制,当AOF文件的大小超过设定的阈值时,Redis就会启动AOF内容压缩,只保留最小的指令集。
        2. 可以使用bgrewriteaof命令重写
      1. 重写过程中,AOF文件被更改怎么办?
      1. AOF的优势
        1. AOF提供多种同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据
      1. AOF缺点
        1. 对具有相同的数据,AOF的文件通常会比RDB文件体积大
    1. RDB和AOF如何选择?
      1. img

Redis高级应用

高性能、高可用、扩展性方案主要依赖两种技术:分片和冗余。分片就是把数据拆分到多个节点存储;冗余就是每个节点都有一个或者多个副本。

1. Redis主从复制
    1. 主从复制原理
      1. 连接阶段,salve节点启动时,会在自己本地保存master节点的信息,包括master node的host和port;slave内部有个定时任务,每隔1秒会检查是否有新master要连接和复制;为了让主节点感知slave节点,slave会定时向主节点发送ping请求
      2. 数据同步阶段
        1. 如果是新加入的slave节点,需要全量复制。master通过bgsave命令在本地生成一份RDB快照发送给slave。
        2. 如果slave节点自己本来有数据怎么办?slave节点首先需要清除自己数据,然后用RDB快照加载数据
        3. master节点成成RDB期间,接收到的写命令怎么处理?开始生成RDB文件时,master会把所有新的写命令放入缓存。在slave节点保存了RDB之后,再将新的写命令复制给slave节点(跟AOF 重写rewrite期间接收到新命令的处理思路一样)
      1. 命令传播阶段
        1. master持续把写命令,异步复制给slave 节点
      1. 主从复制的不足:
        1. 没有解决高可用问题,如果master节点挂了,对外提供的服务就不可用,没有解决单点问题
        2. 每次都是手动把之前的从节点改为主节点,比较费时费力,会造成一定服务不可用
      1. 其他
        1. slave节点通过master_repl_offset记录偏移量,避免全量复制
        2. Redis6.0的一个新特性,为了降低主节点的磁盘开销。主从复制的无盘复制,master生成的RDB文件不保存到磁盘,而是直接通过网络发送给从节点。
2. Sentinel 哨兵
    1. Redis通过Sentinel(哨兵)来解决高可用。为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel即监控所有Redis服务,Sentinel之间也相互监控。img
    2. Sentinel本身没有主从之分,地位平等,只有Redis节点之间有主从之分;Sentinel是一个特殊状态的Redis节点,具有发布订阅功能;哨兵在上线时,给所有Redis节点的名字为:sentinel:hello的channel发送消息,所以能相互感知对方的存在,而进行监控。
    3. Sentinel的主要作用:感知Redis节点服务的状态和故障迁移
      1. 感知master节点下线
        1. Sentinel默认以每秒1次的频率向Redis服务节点发送PING命令,如果在指定的时间内(默认30s)没收到有效回复,Sentinel将该服务标记下线(主观下线)
        2. 但是,只有你发现master下线了,并不代表master真的下线了。该Sentinel会询问其他的Sentinel节点,如果多数的Sentinel都认为该master下线,master才真正下线(客观下线)
      1. 故障迁移
        1. Redis的选举和故障迁移是由Sentinel完成的;故障迁移第一步就是选举一个Leader,由Leader完成故障迁移流程,Sentinel通过Raft算法完成选举。
        2. Raft算法是一个共识算法,核心思想是:先到先得,少数服从多数。Raft选举Leader过程。
        3. Reids的Raft算法和Raft论文有所不同:
          1. master的客观下线触发选举,而不是通过election timeout的时间开始选举
          2. Leader并不会把自己成为Leader的消息发送给其他Sentinel,其他Sentinel等待Leader选出master后,检测到新的master正常工作后,就会去掉客观下线标识,不需要进入故障转移流程。
        1. 完成上面的步骤,我们只是从Sentinel节点里面选举出一个Leader,下面是从slave节点选举出master节点规则:img
        2. 确定master节点后,如何让其他的节点变成他的从节点呢?选举出master后,由Sentinel Leader 向某个节点发送slaveof no one命令,让它成为独立节点;然后向其他节点发送slaveof x.x.x.x xxxx(本机IP端口),让他们成为这个节点的从节点,故障转移完成。
    1. Sentinel的不足:
      1. 主从切换的时候,会有数据丢失,因为只有一个master
      2. 只能单点写,没有解决水平扩容问题
3. Redis分布式方案

第一种在客户端实现先关逻辑,例如使用一致性哈希对key进行分片;

第二种把分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求转发;

第三种是基于服务端实现;

a. 客户端分片
      1. 普通哈希,hash(key)%N,根据余数,决定映射到哪一个节点。这种方式比较简单属于静态的分片规则,一旦节点数量发生变化,由于取模的N发生变化,数据需要重新分配。未解决这个问题,我们有一致性哈希算法。
      2. 一致性哈希的原理:
        1. imgimg
        2. 一致性哈希动态增减节点,只会影响到相邻节点,对其他节点没有影响。但是一致性哈希有一个问题,因为节点不是均匀分布的,特别是节点比较少的情况下,数据不能均匀的分布。解决这个问题的办法就是引入虚拟节点。
b. 代理分片
      1. 存在不足:
        1. 代理架构一般需要借助其他的组件,出现故障不能自主转移,机构复杂。
        2. 扩缩容需要修改配置,不能平滑的扩缩容
      1. 常见的代理中间件:Codis、Twemproxy
c. Redis Cluster
      1. 用来解决高可用,是一个去中心化的架构。以3主3从为例,节点之间两两交互,共享数据分片和节点状态等信息。
      2. Cluster怎么解决分片问题,数据怎么分布?
        1. Cluster没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现。Redis创建了16384个槽(slot),每个节点负责一定区域的slot。
        2. 对象分布到Redis节点时,对key做CRC16算法计算再%16384,得到一个slot的值,数据落到负责这个slot的节点上。Redis中每个master节点都会维护自己负责的slot。
        3. key和slot的关系是永远不会变的,会变的只是slot和redis节点的关系。
      1. 怎么让相关联的数据落到同一个节点上?
        1. 比如有些multi key操作是不能跨节点的,需要落到同一个节点。在key中加入{hash tag}即可。Redis在计算槽编号的时候只会取{}中的字符串进行槽编号计算,{}中的字符串是相同的,因此他们会落到同一个槽。
      1. 客户端连接到哪一台服务器?访问的数据不在当前节点上怎么办?
        1. 服务端返回MOVED,根据key计算出来的slot不归某个节点管理时,服务端会返回具体的节点端口
      1. 新增或下线了master节点,数据怎么迁移?
        1. 因为key和slot的关系是永远不变的,新增了节点需要把原有的slot分配给新的节点,并把相关的数据迁移过来。
      1. 只有主节点可以写,一个主节点挂了,从节点怎么变成主节点?
      1. Redis Cluster的特点

Redis实战

a. 常见Redis客户端
    1. Jedis
      1. Jedis最常见的客户端。
      2. 存在不足地方:多线程使用一个连接的时候线程不安全。解决思路:使用连接池,为每个请求创建不同的连接。
    1. Lettuce
      1. 克服了线程不安全问题,Lettuce是一个可伸缩的线程安全的Redis客户端,多个线程可以共享一个线程示例。
    1. Redisson
      1. 是一个在Redis的基础上实现的Java内存数据网格,提供了分布式和可扩展的Java数据结构。比如分布式的Map、List等
      2. Redisson使用分布式锁的实现原理:
        1. 最终也是调用一段Lua脚本。
        2. 业务没执行完,锁到期了怎么办?答:watch dog,类似存在定时检查锁的过期时间,不断续命。
        3. 集群模式下,如果对多个master加锁,导致重复加锁怎么办?Redisson会自动选择同一个master。
b. 数据一致性
    1. 我们的原则:数据最终以数据库为准。
    2. 如果我们既要操作数据库,也要操作Redis,存在两种选择:
      1. 先操作Redis数据再操作数据库数据
      2. 先操作数据库数据再操作Redis数据
      3. Redis数据:删除还是更新?主要考虑缓存的代价,如果更新缓存之前,需要经过其他表的查询、接口调用和复杂的计算,建议直接删除缓存,这种方案更加简单。
      4. 先删除缓存,再更新数据库
        1. 正常情况,缓存删除成功,数据库成功
        2. 异常情况
          1. 删除缓存失败,程序捕获异常,不会走到下一步,不会出现数据不一致
          2. 删除缓存成功,更新数据库失败。因为以数据库数据为准,所以不存在数据不一致问题
          3. 数据并发的情况:

          4. 解决办法:使用延时双删的策略,在写入数据后,再删除一次缓存。
              1. A线程:删除缓存
              2. 更新数据库
              3. 休眠500ms(这个时间依据读取数据时间)
              4. 再次删除缓存
      1. 先更新数据库,再删除缓存
        1. 正常情况下:更新数据库成功,删除缓存成功
        2. 异常情况:
          1. 更新数据库失败,程序捕获异常,不会走到下一步,不会出现数据不一致
          2. 更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,出现缓存不一致。
          3. 解决办法:
            1. 第一种方案:删除缓存失败,可以捕获异常,将key发送到消息队列进行重试。但是会造成代码入侵
            2. 第二种方案:通过服务来监听binlog,例如(阿里的canal)在服务端完成删除key的操作,如果失败再发送消息队列。
            3. 总之对删除缓存失败,做法都是不断尝试删除,直到成功
c. 高并发问题
    1. 热点数据发现,如何找出高频率的Key?
      1. 第一种客户端统计,对set/get命令进行记数
      2. 第二种在代理层实现计数
      3. 第三种在服务端统计,Redis有一个monitor命令,可以监控Redis执行命令。facebook的开源项目redis-faina就是基于这个原理实现
    1. 缓存雪崩
      1. 缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,导致并发量大的时所有请求都落到数据库。
      2. 解决方案:
        1. 在高并发时进行限流,使用互斥锁或者队列,针对同一个key只允许一个线程查询数据库
        2. 缓存预热,避免同时失效
        3. 设置随机过期时间
        4. 数据永不过期
    1. 缓存击穿
    1. 缓存穿透
      1. 缓存穿透指的是查询一个不存在的数据,缓存层和持久层都不会命中,导致每次请求都落到持久层。
      2. 解决方案:
        1. 缓存空数据、缓存特殊字符串,比如:&&
        2. 布隆过滤器
          1. 布隆过滤器的本质就是:一个位数组和若干个哈希函数
          2. 布隆过滤器的特点:
            1. 如果布隆过滤器判断元素在集合中存在,不一定存在
            2. 如果布隆过滤器判断不存在,一定不存在
        1. . 使用布隆过滤器可以解决缓存穿透