Redis 基础
什么是 Redis?
- Redis (Remote Dictionary Server) 本质上是一个 Key-Value 类型的内存数据库,很像 memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。
- 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。
- Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性能消息队列服务,用他的 Set 可以做高性能的 tag 系统等等。
- 另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一个功能加强版的 memcached 来用。 Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 为什么这么快?
- Redis 完全基于内存,绝大部分请求是纯粹的内存操作,数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度是O(1)。
- Redis 的数据结构简单,对数据操作也简单。
- Redis 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的 CPU 切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。
- Redis 使用多路复用 IO 模型,非阻塞 IO。
Redis 的使用场景?
- 会话缓存(Session Cache)
- 用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。
- 全页缓存(FPC)
- 除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。
- 以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能以最快速度加载你曾浏览过的页面。
- 队列
- Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。
- 排行榜/计数器
- Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单。
- 发布/订阅
- Redis 的发布/订阅功能使用场景非常多。在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!
Redis 的数据类型?
五种基础数据结构:
- String:字符串,是构建其他数据结构的基础,一个字符串类型的值存储的最大容量是 512M。
- Hash:哈希列表
- List:列表
- Set:集合,在哈希列表的基础上实现
- Sort Set:有序集合
复杂的数据结构:
- Bitmaps:位图,在 string 的基础上进行位操作,可以实现节省空间的数据结构。
- Hyperloglog:用于估计⼀个 set 中元素数量的概率性的数据结构。
- Geo:geospatial,地理空间索引半径查询。
- BloomFilter:布隆过滤器。
Redis 与 memcached?
Redis 与 memcached 区别:
- 存储方式上:memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
- 数据类型上:memcache 对数据类型的支持简单,只支持简单的 key-value,而 Redis 支持五种数据类型。
- 底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不⼀样。Redis 直接自己构建了 VM 机制,因为⼀般的系统调用系统函数的话,会浪费⼀定的时间去移动和请求。
- value 的大小:Redis 可以达到1GB,而 memcache 只有1MB。
Redis 与 memcached 选择:
选择 Redis 的情况:
- 复杂数据结构,value 的数据是哈希,列表,集合,有序集合等这种情况下,会选择 Redis, 因为 memcache 无法满足这些数据结构,最典型的的使用场景是,用户订单列表,用户消息,帖子评论等。
- 需要进行数据的持久化功能,但是注意,不要把 Redis 当成数据库使用,如果 Redis 挂了,内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有 cache 预热的过程。对于只读和数据一致性要求不高的场景可以采用持久化存储
- 高可用,Redis 支持集群,可以实现主动复制,读写分离,而对于 memcache 如果想要实现高可用,需要进行二次开发。
- 存储的内容比较大,memcache 存储的 value 最大为 1M。
选择 memcache 的场景:纯 KV,数据量非常大的业务,原因是:
- memcache 的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间,Redis 是临时申请空间,可能导致碎片化。
- 虚拟内存使用,memcache 将所有的数据存储在物理内存里,Redis 有自己的 vm 机制,理论上能够存储比物理内存更多的数据,当数据超量时,引发 swap,把冷数据刷新到磁盘上,从这点上,数据量大时,memcache 更快。
- 网络模型,memcache 使用非阻塞的 IO 复用模型,Redis 也是使用非阻塞的 IO 复用模型,但是 Redis 还提供了一些非 KV 存储之外的排序,聚合功能,复杂的 CPU 计算,会阻塞整个 IO 调度,从这点上由于 Redis 提供的功能较多,memcache 更快些。
- 线程模型,memcache 使用多线程,主线程监听,worker 子线程接受请求,执行读写,这个过程可能存在锁冲突。Redis 使用的单线程,虽然无锁冲突,但是难以利用多核的特性提升吞吐量。
Jedis 与 Redisson?
Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;
Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redis 集群
什么是 Redis 集群?
Redis 集群:
Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许Redis在不同节点上同时提供服务,提高整体性能和可靠性。
Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现: 一个 Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
Redis 集群的优点:
- 高可用性:Redis 集群可以在某个节点发生故障时,自动进行故障转移,保证服务的持续可用。
- 负载均衡:Redis 集群可以将客户端请求分发到不同的节点上,有效地分摊节点的压力,提高系统的整体性能。
- 容灾恢复:通过主从复制或哨兵模式,Redis 集群可以在主节点出现故障时,快速切换到从节点,实现业务的无缝切换。
- 数据分片:在 Cluster 模式下,Redis 集群可以将数据分散在不同的节点上,从而突破单节点内存限制,实现更大规模的数据存储。
- 易于扩展:Redis 集群可以根据业务需求和系统负载,动态地添加或移除节点,实现水平扩展。
Redis 集群模式:
- 主从复制模式(Master-Slave):适用于数据备份和读写分离场景,配置简单,但在主节点故障时需要手动切换。
- 哨兵模式(Sentinel):在主从复制的基础上实现自动故障转移,提高高可用性,适用于高可用性要求较高的场景。
- Cluster 模式:通过数据分片和负载均衡实现大规模数据存储和高性能,适用于大规模数据存储和高性能要求场景。
主从复制模式?
主从复制原理:
主从复制是 Redis 的一种基本集群模式,它通过将一个 Redis 节点(主节点)的数据复制到一个或多个其他 Redis 节点(从节点)来实现数据的冗余和备份。主节点负责处理客户端的写操作,同时从节点会实时同步主节点的数据。客户端可以从从节点读取数据,实现读写分离,提高系统性能。
主从复制的优缺点:
- 优点:
- 配置简单,易于实现。
- 实现数据冗余,提高数据可靠性。
- 读写分离,提高系统性能。
- 缺点:
- 主节点故障时,需要手动切换到从节点,故障恢复时间较长。
- 主节点承担所有写操作,可能成为性能瓶颈。
- 无法实现数据分片,受单节点内存限制。
主从复制应用场景:
- 数据备份和容灾恢复:通过从节点备份主节点的数据,实现数据冗余。
- 读写分离:将读操作分发到从节点,减轻主节点压力,提高系统性能。
- 在线升级和扩展:在不影响主节点的情况下,通过增加从节点来扩展系统的读取能力。
主从复制模式适合数据备份、读写分离和在线升级等场景,但在主节点故障时需要手动切换,不能自动实现故障转移。如果对高可用性要求较高,可以考虑使用哨兵模式或 Cluster 模式。
主从复制配置和实现:
配置主节点:在主节点的 redis.conf 配置文件中,无需进行特殊配置,主节点默认监听所有客户端请求。
# 主节点默认端口号6379
port 6379
配置从节点:在从节点的 redis.conf 配置文件中,添加如下配置,指定主节点的地址和端口。
# 从节点设置端口号6380
port 6380# replicaof 主节点IP 主节点端口
replicaof 127.0.0.1 6379
或者,通过 Redis 命令行在从节点上执行如下命令:
redis> replicaof 127.0.0.1 6379
哨兵模式?
哨兵模式原理:
哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。哨兵节点是一种特殊的 Redis 节点,它会监控主节点和从节点的运行状态,当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移。
哨兵模式的优缺点:
- 优点:
- 自动故障转移,提高系统的高可用性。
- 具有主从复制模式的所有优点,如数据冗余和读写分离。
- 缺点:
- 配置和管理相对复杂。
- 依然无法实现数据分片,受单节点内存限制。
哨兵模式应用场景:
- 高可用性要求较高的场景:通过自动故障转移,确保服务的持续可用。
- 数据备份和容灾恢复:在主从复制的基础上,提供自动故障转移功能。
哨兵模式在主从复制模式的基础上实现了自动故障转移,提高了系统的高可用性。然而,它仍然无法实现数据分片。如果需要实现数据分片和负载均衡,可以考虑使用 Cluster 模式。
哨兵模式配置和实现:
配置主从复制:首先按照主从复制模式的配置方法,搭建一个主从复制集群。
配置哨兵节点:在哨兵节点上创建一个新的哨兵配置文件(如:sentinel.conf),并添加如下配置:
# sentinel节点端口号
port 26379# sentinel monitor 被监控主节点名称 主节点IP 主节点端口 quorum
sentinel monitor mymaster 127.0.0.1 6379 2# sentinel down-after-milliseconds 被监控主节点名称 毫秒数
sentinel down-after-milliseconds mymaster 60000# sentinel failover-timeout 被监控主节点名称 毫秒数
sentinel failover-timeout mymaster 180000
- quorum 是指触发故障转移所需的最小哨兵节点数。
- down-after-milliseconds 表示主节点被判断为失效的时间。
- failover-timeout 是故障转移超时时间。
启动哨兵节点:
redis> redis-sentinel /path/to/sentinel.conf
Cluster 模式?
Cluster 模式原理:
Cluster 模式是 Redis 的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。在 Cluster 模式下,Redis 将所有的键值对数据分散在多个节点上,每个节点负责一部分数据,称为槽位。通过对数据的分片,Cluster 模式可以突破单节点的内存限制,实现更大规模的数据存储。
Cluster 模式的优缺点:
- 优点:
- 数据分片,实现大规模数据存储。
- 负载均衡,提高系统性能。
- 自动故障转移,提高高可用性。
- 缺点:
- 配置和管理较复杂。
- 一些复杂的多键操作可能受到限制。
Cluster 模式应用场景:
- 大规模数据存储:通过数据分片,突破单节点内存限制。
- 高性能要求场景:通过负载均衡,提高系统性能。
- 高可用性要求场景:通过自动故障转移,确保服务的持续可用。
Cluster 模式在提供高可用性的同时,实现了数据分片和负载均衡,适用于大规模数据存储和高性能要求的场景。然而,它的配置和管理相对复杂,且某些复杂的多键操作可能受到限制。
Cluster 模式配置和实现:
配置 Redis 节点:为每个节点创建一个 redis.conf 配置文件,并添加如下配置:
# cluster节点端口号
port 7001# 开启集群模式
cluster-enabled yes# 节点超时时间
cluster-node-timeout 15000
启动 Redis 节点:
redis> redis-server redis_7001.conf
创建 Redis Cluster:
redis> redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1
- cluster-replicas 表示从节点的数量,1代表每个主节点都有一个从节点。
Redis 集群不可用情况?
有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。
Redis 集群是如何复制的?
异步复制
Redis 集群如何选择数据库?
Redis 集群目前无法做数据库选择,默认在 0 数据库。
Redis 持久化
什么是 Redis 持久化?
Redis 持久化:
Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失。因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。
Redis 的持久化机制有两种:
- RDB 快照
- AOF 日志
两种机制的对比:
- 快照 RDB 是一次全量备份,AOF 日志是连续的增量备份。
- 快照 RDB 是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。
- AOF 日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载 AOF 日志进行指令重放的时间就会很长,所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
Redis 如何扩容?
- 如果 Redis 被当做缓存使用,使用一致性哈希实现动态扩容缩容。
- 如果 Redis 被当做一个持久化存储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis 集群可以做到这样。
Redis 优化
Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。
Redis 综合问题
Redis 缓存击穿?
缓存击穿是指,要查询的数据在 Redis 中存在,但在 Redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。
缓存击穿解决方案:
- 加锁,当查询缓存没有的时候就加锁,然后再从数据库查询并重设缓存值,然后再释放锁。
Redis 缓存穿透?
缓存穿透是指,要查询的数据在缓存中和数据库中都不存在,每次针对此这条数据的请求从缓存获取不到,都会请求到数据库,从而可能压垮数据库。
缓存穿透解决方案:
- 校验请求参数正确性
- 缓存空对象,缺点就是可能会导致缓存中有大量空值的缓存
- 布隆过滤器
Redis 缓存雪崩?
缓存雪崩是指,缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。Redis 服务器宕机也会导致缓存雪崩。
缓存雪崩解决方案:
- 给缓存值设置不同的失效时间
- 降级、熔断
- Redis 集群
Redis 与数据库的一致性?
- 强一致性:任何一次读取数据都能读到某个数据的最近一次写的数据。
- 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分数据或者全部访问不到,则是弱一致性。
保证一致性的方案:
- 延迟双删
- 通过MQ进行重试
- 数据库 binlog 异步删除
- 分布式锁
- 带版本写入
常见分布式锁方案?
分类 | 方案 | 实现原理 | 优缺点 |
基于数据库 | 基于mysql 表唯一索引 | 表增加唯一索引 加锁:执行insert语句,若报错,则表明加锁失败 解锁:执行delete语句 | 优点:
缺点:
|
基于MongoDB findAndModify原子操作 | 加锁:执行findAndModify原子命令查找document,若不存在则新增 解锁:删除document | 优点:
缺点:
| |
基于分布式协调系统 | 基于ZooKeeper | 加锁:在 解锁:删除节点 | 优点:
缺点:
|
基于缓存 | 基于redis命令 | 加锁:执行setnx,若成功再执行expire添加过期时间 解锁:执行delete命令 | 优点:
缺点:
|
基于redis Lua脚本能力 | 加锁:执行 解锁:执行Lua脚本,释放锁时验证 random_value -- ARGV[1]为random_value, KEYS[1]为lock_name if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end | 优点:
缺点:
|
分布式锁需满足四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
Redis 实现分布式锁?
Redis 实现分布式锁原理
Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系,基于此,Redis 中可以使用 SETNX(SET if Not Exists) 命令实现分布式锁。
setnx key value
将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。如果需要解锁,使用 del key 命令就能释放锁。
(3)解决死锁
如果一个持有锁的客户端失败或崩溃了不能释放锁,那么就会出现死锁的问题。这时就可以给锁设置一个过期时间,可以通过两种方法实现:
通过命令 setnx 键名 过期时间;
使用 setnx key “当前系统时间+锁持有的时间” 和 getset key “当前系统时间+锁持有的时间” 组合的命令就可以实现。
具体做法如下:
- 客户端2发送 SETNX lock.test 想要获得锁,由于之前的客户端1还持有锁,所以 Redis 返回一个0
- 客户端2发送 GET lock.test 以检查锁是否超时,如果没超时则等待或重试。如果已超时,客户端2通过 GETSET lock.test 过期的时间 操作来尝试获得锁,通过GETSET,客户端2拿到的时间戳如果是超时的,那就说明客户端2如愿以偿拿到锁了。
- 如果在客户端2之前,有个客户端3比客户端2快一步执行了上面的操作,那么客户端2拿到的时间戳是个未超时的值,这时,说明客户端2没有如期获得锁,需要再次等待或重试。
- 尽管客户端2没拿到锁,但它改写了客户端3设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
通过设置锁的 expire 时间,让 Redis 去删除锁。
通过 Redis中expire() 给锁设定最大持有时间,如果超过,则 Redis 来帮我们释放锁。
- 客户端1使用 setnx 获得了锁,并且使用 expire 设定一个过期时间,假定是10ms
- 过了4ms后,客户端1不幸运的宕机了,此时客户端2想要通过 setnx 尝试获得锁,但是锁还没有过期,任然被客户端1所持有。
- 到了 11ms 时,锁过期了,Redis 帮我们删除了锁,此时客户端2通过 setnx 就能成功获得锁。
Redis 分区?
Redis 分区:
分区可以让 Redis 管理更大的内存,Redis 将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使 Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。
Redis 分区实现方案:
客户端分区:就是在客户端就已经决定数据会被存储到哪个 Redis 节点或者从哪个 Redis 节点读取,大多数客户端已经实现了客户端分区。
代理分区:意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些 Redis 实例,然后根据 Redis 的响应结果返回给客户端。Redis 和 memcached 的一种代理实现就是 Twemproxy。
查询路由(Query routing) 的意思是客户端随机地请求任意一个 Redis 实例,然后由 Redis 将请求转发给正确的 Redis 节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个 Redis 节点转发到另一个 Redis 节点,而是在客户端的帮助下直接 redirected 到正确的 Redis 节点。
Redis 分区缺点:
- 涉及多个 key 的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的 Redis 实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
- 同时操作多个 key,则不能使用 Redis 事务。
- 分区使用的粒度是 key,不能使用一个非常长的排序 key 存储一个数据集。
- 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的 Redis 实例和主机同时收集 RDB / AOF 文件。
- 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除 Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
Redis 回收进程如何工作的?
一个客户端运行了新的命令,添加了新的数据。Redis 检查内存使用情况,如果大于 max memory 的限制,则根据设定好的策略进行回收。
所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
Redis 淘汰策略?
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
- allkeys-random:从数据集中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用 no-enviction 策略可以保证数据不被丢失。
Redis 预热?
缓存预热如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至 Redis 内再提供出去使用,这种操作就成为"缓存预热"。
缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。
Redis 更新?
缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。
第二步操作异常:缓存和数据的操作顺序中,第二个动作报错。如数据库被更新, 此时失效缓存的时候出错,缓存内数据仍是旧版本;
缓存更新的设计模式有四种:
- Cache aside:
- 查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;
- 更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;
- Read through:
- 在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;
- Write through:
- 在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;
- Write behind caching:
- 俗称 write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;
Cache aside:
- 为了避免在并发场景下,多个请求同时更新同一个缓存导致脏数据,因此不能直接更新缓存而是另缓存失效。
- 先更新数据库后失效缓存:并发场景下,推荐使用延迟失效(写请求完成后给缓存设置1s过期时间),在读请求缓存数据时若redis内已有该数据(其他写请求还未结束)则不更新。当redis内没有该数据的时候(其他写请求已令该缓存失效),读请求才会更新redis内的数据。这里的读请求缓存数据可以加上失效时间,以防第二步操作异常导致的不一致情况。
- 先失效缓存后更新数据库:并发场景下,推荐使用延迟失效(写请求开始前给缓存设置1s过期时间),在写请求失效缓存时设置一个1s延迟时间,然后再去更新数据库的数据,此时其他读请求仍然可以读到缓存内的数据,当数据库端更新完成后,缓存内的数据已失效,之后的读请求会将数据库端最新的数据加载至缓存内保证缓存和数据库端数据一致性;在这种方案下,第二步操作异常不会引起数据不一致,例如设置了缓存1s后失效,然后在更新数据库时报错,即使缓存失效,之后的读请求仍然会把更新前的数据重新加载到缓存内。
推荐使用先失效缓存,后更新数据库,配合延迟失效来更新缓存的模式;
四种缓存更新模式的优缺点:
- Cache Aside:实现起来较简单,但需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository);
- Read/Write Through:只需要维护一个数据存储(缓存),但是实现起来要复杂一些;
- Write Behind Caching:与Read/Write Through 类似,区别是Write Behind Caching的数据持久化操作是异步的,但是Read/Write Through 更新模式的数据持久化操作是同步的。优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。
缓存本身就是通过牺牲强一致性来提高性能,因此使用缓存提升性能,就会有数据更新的延迟性。这就需要我们在评估需求和设计阶段根据实际场景去做权衡了。
Redis 降级?
- 缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。
- 服务降级的目的,是为了防止 Redis 服务故障,导致数据库跟着一起发生雪崩问题。 因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis 出现问题,不去数据库查询,而是直接返回默认值给用户。
- 降级的目的是保证核心服务可用,即使是有损的。如双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。
- 降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于对系统功能的取舍。
Redis 常见性能问题和解决?
- Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
- 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...,这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。