Redis Concurrency
Redis原理
1. Redis支持发布订阅模式
-
- 略
2. Redis事务
-
- Redis的单个命令是原子性的,要么成功要么失败,不存在并发干扰问题。如果涉及多个命令,需要把多个命令作为一个不可分割的处理序列,Redis提供事务的功能:
-
-
- 按进入队列的顺序进行
- 不会收到其他客户端请求的影响
- 事务不能嵌套,多个multi命令效果一样
-
-
- Redis的事务涉及四个命令,multi(开启事务)、exec(执行事务)、discard(取消事务)、watch(监视)
-
-
- 通过multi命令开启事务。multi执行后,客户端可以继续向服务器发送任意多条命令,不会立即执行而是被放到一个队列。
- 执行exec时,所有队列中命令才会被执行;如果没有执行exec,所有命令都不会被执行
- discard可以清空事务队列,放弃执行
- watch命令,防止事务过程中某个key的值被其他客户端请求修改,带来非预期的结果。即多个客户端更新变量的时候,会跟原值比较,只有他没有被其他线程修改的情况下才更新成新值。
-
-
-
-
- 我们可以用watch监视一个或多个key,如果开启事务后,至少有一个被监视的key键在exec执行之前被修改了,那么整个事务都会被取消
-
-
-
-
- 事务可能遇到的问题
-
-
-
-
- 在执行exce之前发生错误,例如语法错误,包括参数名,参数数量(编译错误)等。事务会被拒绝执行,队列中所有命令都不会执行。
- 在执行exce之后发生错误,例如数据类型错误(运行时错误)等。只有错误的命令没有被执行,但是其他命令没有被影响
- 我们没发用Redis的这种事务机制来实现原子性,保证数据一致性。
-
-
3. Lua脚本
-
- 使用Lua脚本来执行Redis命令为好处:
-
-
- 一次发送多个命令,减少网络开销
- Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性
- 对于复杂的组合命令,我们可以放在文件中,实现命令复用
-
-
- Lua使用技巧:
-
-
- 缓存Lua脚本。在脚本比较长的情况下,每次调用脚本都需要把整个脚本都传给Redis服务,会产生较大的网络开销。解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以通过摘要码来执行Lua脚本。两个命令:
-
-
-
-
- script load “return ‘Hello World’”
- evalsha “摘要码”
-
-
-
-
- 脚本超时。脚本执行有一个超时时间,默认为5秒钟。超过5秒钟,其他客户端命令不会等待,直接返回“BUSY”错误。这样也不行,不能一直拒绝其他客户端命令执行,提供两个命令:
-
-
-
-
- script kill,终止脚本的执行,但是遇见set、del命令,会返回UNKILLABLE错误。原因是为了保证脚本运行的原子性。
- 遇见上面的情况只能通过shutdown nosave 命令,直接把Redis服务停掉。
-
-
4. Redis为什么这么快
-
- Redis一般情况下支持并发大概10万QPS左右
- Redis快主要包括以下3点:
-
-
- 纯内存结构
-
-
-
-
- KV结构的内存数据库,时间复杂度O(1)。
-
-
-
-
- 请求处理单线程,处理客户端请求是单线程的,这样的好处:
-
-
-
-
- 没有创建线程,销毁线程带来的消耗
- 避免了上下文切换导致的CPU消耗
- 避免线程之间带来的竞争问题,例如加锁释放锁等等
-
-
-
-
- I/O多路复用机制
-
-
-
-
- 基本原理就是不再是由应用程序自己监控连接,而是由内核替应用程序监视文件描述符
- 常见的多路复用器.
-
-
5. 内存回收
-
- 过期策略
-
-
- 立即过期(主动淘汰),每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期数据,对内存友好;但是会占用大量的CPU资源去处理过期数据,从而会影响缓存的响应时间和吞吐量
- 惰性过期(被动淘汰),只有访问一个key时,才会判断该key是否过期,过期则清除。该策略可以最大化地节省CPU资源,对内存非常不友好。
- 定期过期,每隔一段时间会扫描一定数据库的expires字典中的一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。
- 总结:Redis中同时使用了惰性过期和定期过期两种过期策略,并不是实时地清除过期key
-
-
- 淘汰策略
-
-
- 前提:Redis内存淘汰策略是指,内存使用达到最大的内存极限时,需要使用淘汰算法来清理掉哪些数据,以保证新数据的存入。
- 最大内存设置:maxmemory,如果不设置或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存
- 淘汰策略,大概分为几种策略
-
-
-
-
- LRU,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰
- LFU,最不常用,按照使用频率删除
- random,随机删除
- 总结:noeviction默认策略,不删除任何数据,决绝所有写入操作比返回OOM
- LRU原理:略
- LFU原理:略
-
-
6. 持久化机制
-
- Redis提供两种持久化方案,RDB快照和AOF。持久化是Redis和Memcache的主要区别之一
- RDB,是Redis默认的持久化方案(如果开启了AOF,优先使用AOF)。当满足一定的条件,会把内存中的数据写入磁盘,生成一个快照文件dump.rdb。
-
-
- RDB触发
-
-
-
-
- 自动触发
-
-
-
-
-
-
- 配置规则触发,redis.conf,定义了触发把数据保存在磁盘的触发频率
- shutdown触发,保证服务器正常关闭
- flushall触发备份,单rdb文件是空的
-
-
-
-
-
-
- 手动触发
-
-
-
-
-
-
- save
- bgsave
-
-
-
-
-
- RDB的优势
-
-
-
-
- RDB是一个非常紧凑(compact)的文件,它保存了redis在某个时间点上的数据集。非常适用于进行备份和灾难恢复
- 生成RDB文件时,Redis主进程会fork一个子进程来处理所有保存工作,主进程不需要进行任何磁盘工作
- RDB在恢复大数据集时的速度比AOF的更快
-
-
-
-
- RDB的劣势
-
-
-
-
- 没办法做到实时持久化。因为bgsave每次运行都要执行fork创建子进程,执行成本过高
- 在一定间隔时间做一次备份,如果redis意外down掉,就会丢失最后一次快照之后的所有修改
-
-
-
- AOF(Append Only File)
-
-
- 默认不开启。AOF采用日志的形式来记录每个操作,并追加到文件中。只要执行Redis命令,就会把命令写入到AOF文件中
- 数据都是实时持久化到磁盘的吗?
-
-
-
-
- 由于操作系统的缓存机制,AOF数据并没有实时地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?
-
-
-
-
- 文件越来越大怎么办?
-
-
-
-
- 随着AOF的文件越来越大,文件越大,占用服务器内存越来越大以及AOF恢复时间越长。可能会出现一个问题:记录的命令很多都是重复的,有效的结果很少。为了解决这个问题,Redis增加了重写机制,当AOF文件的大小超过设定的阈值时,Redis就会启动AOF内容压缩,只保留最小的指令集。
- 可以使用bgrewriteaof命令重写
-
-
-
-
- 重写过程中,AOF文件被更改怎么办?
-
-
-
- AOF的优势
-
-
-
-
- AOF提供多种同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据
-
-
-
-
- AOF缺点
-
-
-
-
- 对具有相同的数据,AOF的文件通常会比RDB文件体积大,
-
-
-
- RDB和AOF如何选择?
-
-
Redis高级应用
高性能、高可用、扩展性方案主要依赖两种技术:分片和冗余。分片就是把数据拆分到多个节点存储;冗余就是每个节点都有一个或者多个副本。
1. Redis主从复制
-
- 主从复制原理
-
-
- 连接阶段,salve节点启动时,会在自己本地保存master节点的信息,包括master node的host和port;slave内部有个定时任务,每隔1秒会检查是否有新master要连接和复制;为了让主节点感知slave节点,slave会定时向主节点发送ping请求
- 数据同步阶段
-
-
-
-
- 如果是新加入的slave节点,需要全量复制。master通过bgsave命令在本地生成一份RDB快照发送给slave。
- 如果slave节点自己本来有数据怎么办?slave节点首先需要清除自己数据,然后用RDB快照加载数据
- master节点成成RDB期间,接收到的写命令怎么处理?开始生成RDB文件时,master会把所有新的写命令放入缓存。在slave节点保存了RDB之后,再将新的写命令复制给slave节点(跟AOF 重写rewrite期间接收到新命令的处理思路一样)
-
-
-
-
- 命令传播阶段
-
-
-
-
- master持续把写命令,异步复制给slave 节点
-
-
-
-
- 主从复制的不足:
-
-
-
-
- 没有解决高可用问题,如果master节点挂了,对外提供的服务就不可用,没有解决单点问题
- 每次都是手动把之前的从节点改为主节点,比较费时费力,会造成一定服务不可用
-
-
-
-
- 其他
-
-
-
-
- slave节点通过master_repl_offset记录偏移量,避免全量复制
- Redis6.0的一个新特性,为了降低主节点的磁盘开销。主从复制的无盘复制,master生成的RDB文件不保存到磁盘,而是直接通过网络发送给从节点。
-
-
2. Sentinel 哨兵
-
- Redis通过Sentinel(哨兵)来解决高可用。为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel即监控所有Redis服务,Sentinel之间也相互监控。
- Sentinel本身没有主从之分,地位平等,只有Redis节点之间有主从之分;Sentinel是一个特殊状态的Redis节点,具有发布订阅功能;哨兵在上线时,给所有Redis节点的名字为:sentinel:hello的channel发送消息,所以能相互感知对方的存在,而进行监控。
- Sentinel的主要作用:感知Redis节点服务的状态和故障迁移
- Redis通过Sentinel(哨兵)来解决高可用。为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel即监控所有Redis服务,Sentinel之间也相互监控。
-
-
- 感知master节点下线
-
-
-
-
- Sentinel默认以每秒1次的频率向Redis服务节点发送PING命令,如果在指定的时间内(默认30s)没收到有效回复,Sentinel将该服务标记下线(主观下线)
- 但是,只有你发现master下线了,并不代表master真的下线了。该Sentinel会询问其他的Sentinel节点,如果多数的Sentinel都认为该master下线,master才真正下线(客观下线)
-
-
-
-
- 故障迁移
-
-
-
-
- Redis的选举和故障迁移是由Sentinel完成的;故障迁移第一步就是选举一个Leader,由Leader完成故障迁移流程,Sentinel通过Raft算法完成选举。
- Raft算法是一个共识算法,核心思想是:先到先得,少数服从多数。Raft选举Leader过程。
- Reids的Raft算法和Raft论文有所不同:
-
-
-
-
-
-
- master的客观下线触发选举,而不是通过election timeout的时间开始选举
- Leader并不会把自己成为Leader的消息发送给其他Sentinel,其他Sentinel等待Leader选出master后,检测到新的master正常工作后,就会去掉客观下线标识,不需要进入故障转移流程。
-
-
-
-
-
-
- 完成上面的步骤,我们只是从Sentinel节点里面选举出一个Leader,下面是从slave节点选举出master节点规则:
- 确定master节点后,如何让其他的节点变成他的从节点呢?选举出master后,由Sentinel Leader 向某个节点发送slaveof no one命令,让它成为独立节点;然后向其他节点发送slaveof x.x.x.x xxxx(本机IP端口),让他们成为这个节点的从节点,故障转移完成。
- 完成上面的步骤,我们只是从Sentinel节点里面选举出一个Leader,下面是从slave节点选举出master节点规则:
-
-
-
- Sentinel的不足:
-
-
- 主从切换的时候,会有数据丢失,因为只有一个master
- 只能单点写,没有解决水平扩容问题
-
3. Redis分布式方案
第一种在客户端实现先关逻辑,例如使用一致性哈希对key进行分片;
第二种把分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求转发;
第三种是基于服务端实现;
a. 客户端分片
-
-
- 普通哈希,hash(key)%N,根据余数,决定映射到哪一个节点。这种方式比较简单属于静态的分片规则,一旦节点数量发生变化,由于取模的N发生变化,数据需要重新分配。未解决这个问题,我们有一致性哈希算法。
- 一致性哈希的原理:
-
-
-
-
- 一致性哈希动态增减节点,只会影响到相邻节点,对其他节点没有影响。但是一致性哈希有一个问题,因为节点不是均匀分布的,特别是节点比较少的情况下,数据不能均匀的分布。解决这个问题的办法就是引入虚拟节点。
-
-
b. 代理分片
-
-
- 存在不足:
-
-
-
-
- 代理架构一般需要借助其他的组件,出现故障不能自主转移,机构复杂。
- 扩缩容需要修改配置,不能平滑的扩缩容
-
-
-
-
- 常见的代理中间件:Codis、Twemproxy
-
c. Redis Cluster
-
-
- 用来解决高可用,是一个去中心化的架构。以3主3从为例,节点之间两两交互,共享数据分片和节点状态等信息。
- Cluster怎么解决分片问题,数据怎么分布?
-
-
-
-
- Cluster没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现。Redis创建了16384个槽(slot),每个节点负责一定区域的slot。
- 对象分布到Redis节点时,对key做CRC16算法计算再%16384,得到一个slot的值,数据落到负责这个slot的节点上。Redis中每个master节点都会维护自己负责的slot。
- key和slot的关系是永远不会变的,会变的只是slot和redis节点的关系。
-
-
-
-
- 怎么让相关联的数据落到同一个节点上?
-
-
-
-
- 比如有些multi key操作是不能跨节点的,需要落到同一个节点。在key中加入{hash tag}即可。Redis在计算槽编号的时候只会取{}中的字符串进行槽编号计算,{}中的字符串是相同的,因此他们会落到同一个槽。
-
-
-
-
- 客户端连接到哪一台服务器?访问的数据不在当前节点上怎么办?
-
-
-
-
- 服务端返回MOVED,根据key计算出来的slot不归某个节点管理时,服务端会返回具体的节点端口
-
-
-
-
- 新增或下线了master节点,数据怎么迁移?
-
-
-
-
- 因为key和slot的关系是永远不变的,新增了节点需要把原有的slot分配给新的节点,并把相关的数据迁移过来。
-
-
-
-
- 只有主节点可以写,一个主节点挂了,从节点怎么变成主节点?
-
-
-
- Redis Cluster的特点
-
Redis实战
a. 常见Redis客户端
-
- Jedis
-
-
- Jedis最常见的客户端。
- 存在不足地方:多线程使用一个连接的时候线程不安全。解决思路:使用连接池,为每个请求创建不同的连接。
-
-
- Lettuce
-
-
- 克服了线程不安全问题,Lettuce是一个可伸缩的线程安全的Redis客户端,多个线程可以共享一个线程示例。
-
-
- Redisson
-
-
- 是一个在Redis的基础上实现的Java内存数据网格,提供了分布式和可扩展的Java数据结构。比如分布式的Map、List等
- Redisson使用分布式锁的实现原理:
-
-
-
-
- 最终也是调用一段Lua脚本。
- 业务没执行完,锁到期了怎么办?答:watch dog,类似存在定时检查锁的过期时间,不断续命。
- 集群模式下,如果对多个master加锁,导致重复加锁怎么办?Redisson会自动选择同一个master。
-
-
b. 数据一致性
-
- 我们的原则:数据最终以数据库为准。
- 如果我们既要操作数据库,也要操作Redis,存在两种选择:
-
-
- 先操作Redis数据再操作数据库数据
- 先操作数据库数据再操作Redis数据
- Redis数据:删除还是更新?主要考虑缓存的代价,如果更新缓存之前,需要经过其他表的查询、接口调用和复杂的计算,建议直接删除缓存,这种方案更加简单。
- 先删除缓存,再更新数据库
-
-
-
-
- 正常情况,缓存删除成功,数据库成功
- 异常情况
-
-
-
-
-
-
- 删除缓存失败,程序捕获异常,不会走到下一步,不会出现数据不一致
- 删除缓存成功,更新数据库失败。因为以数据库数据为准,所以不存在数据不一致问题
-
数据并发的情况:
- 解决办法:使用延时双删的策略,在写入数据后,再删除一次缓存。
-
-
-
-
-
-
-
-
-
- A线程:删除缓存
- 更新数据库
- 休眠500ms(这个时间依据读取数据时间)
- 再次删除缓存
-
-
-
-
-
-
-
- 先更新数据库,再删除缓存
-
-
-
-
- 正常情况下:更新数据库成功,删除缓存成功
- 异常情况:
-
-
-
-
-
-
- 更新数据库失败,程序捕获异常,不会走到下一步,不会出现数据不一致
- 更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,出现缓存不一致。
- 解决办法:
-
-
-
-
-
-
-
-
- 第一种方案:删除缓存失败,可以捕获异常,将key发送到消息队列进行重试。但是会造成代码入侵
- 第二种方案:通过服务来监听binlog,例如(阿里的canal)在服务端完成删除key的操作,如果失败再发送消息队列。
- 总之对删除缓存失败,做法都是不断尝试删除,直到成功
-
-
-
-
c. 高并发问题
-
- 热点数据发现,如何找出高频率的Key?
-
-
- 第一种客户端统计,对set/get命令进行记数
- 第二种在代理层实现计数
- 第三种在服务端统计,Redis有一个monitor命令,可以监控Redis执行命令。facebook的开源项目redis-faina就是基于这个原理实现
-
-
- 缓存雪崩
-
-
- 缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,导致并发量大的时所有请求都落到数据库。
- 解决方案:
-
-
-
-
- 在高并发时进行限流,使用互斥锁或者队列,针对同一个key只允许一个线程查询数据库
- 缓存预热,避免同时失效
- 设置随机过期时间
- 数据永不过期
-
-
-
- 缓存击穿
-
-
- 略
-
-
- 缓存穿透
-
-
- 缓存穿透指的是查询一个不存在的数据,缓存层和持久层都不会命中,导致每次请求都落到持久层。
- 解决方案:
-
-
-
-
- 缓存空数据、缓存特殊字符串,比如:&&
- 布隆过滤器:
-
-
-
-
-
-
- 布隆过滤器的本质就是:一个位数组和若干个哈希函数
- 布隆过滤器的特点:
-
-
-
-
-
-
-
-
- 如果布隆过滤器判断元素在集合中存在,不一定存在
- 如果布隆过滤器判断不存在,一定不存在
-
-
-
-
-
-
-
- . 使用布隆过滤器可以解决缓存穿透
-
-