Redis【一、基础】
一、简介
NoSql的一种。性能优越,支持每秒十几万次的读写操作。性能远超数据库,还支持集群、分布式、主从同步等。还支持一定的事务能力。保证了高并发场景下的数据安全和一致性。
二、Redis与Memcache的区别
1、Redis支持更丰富额度数据类型(支持更复杂的应用场景)。Redis不仅仅支持简单的K/V类型的数据。同时提供List,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,string
2、Redis支持数据的持久化。可以将内存中的数据保持在磁盘中,重启的时候,可以再次加载进行使用。memechache把数据全部存储在内存中
3、集群模式。memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。但是Redis目前是原生支持集群(cluster)模式的。
4、Redis使用单线程。Memcached是多线程,非阻塞IO复用的网络模型。Redis使用单线程的多路IO复用模型。
三、Redis单线程问题
3.1、Redis采用单线程,如何保证高并发
【答】
1、完全基于内存;
2、数据结构简单,对数据操作也简单;
3、使用多路I/O复用模型,充分利用CPU资源
3.2、单线程的好处
【答】
1、代码更清晰,处理逻辑更简单
2、不用去考虑各种锁的问题,不存在加锁释放锁的操作,没有因为锁而导致性能消耗
3、不存在多进程或者多线程导致的CPU切换,充分利用CPU资源
四、Redis持久化方案之RDB【快照】
数据持久化的定义:开启持久化功能后,重启Redis后,数据会自动通过持久化文件恢复。
Redis是内存存储,和我们常用的临时数据一致,都是把数据存储在内存中,如果服务器宕机或者Redis当关闭持久化是把快照放到磁盘空间。这两点需要注意。
【流程】
RDB持久化可以使用save或者bgsave,为了不阻塞主进程业务,一般使用bgsave:
1、Redis进程会fork出一个子进程(与父进程内存数据一致)
2、父进程继续处理客户端请求命令
3、由子进程将内存中的所有数据写入到一个临时的RDB文件中
4、完成写入操作后,旧的RDB文件会被新的RDB文件替换
【配置】
save 60 10000:如果在60秒内有10000个key发生改变,那就执行RDB持久化
stop-writes-on-bgsave-error yes:如果Redis执行RDB持久化失败(常见于操作系统内存不足),那么Redis将不再接受Client写入数据的请求。
rdbcompression yes :当生成RDB文件时,同时进行压缩。
dbfilename dump.rdb:将RDB文件命名为dump.rdb。
dir /var/lib/redis :将RDB文件保存在 /var/lib/redis 目录下。
实际情况中,我们通常会将stop-writes-on-bgsave-error设置为fasle,同时让监控系统在Redis执行RDB持久化失败时发送告警,以便人工介入解决,而不是粗暴的拒绝Client的写入请求。
【优点】
1、RDB持久化文件小,Redis数据恢复时速度快
2、子进程不影响父进程,父进程可以持续处理客户端命令
3、子进程fork时采用copy-on-write方式,大多数情况下,没有太多的内存消耗,效率比较好
【缺点】
1、子进程fork时采用copy-on-write方式,如果Redis此时写操作比较多,可能导致额外的内存占用,甚至内存溢出
2、RDB文件压缩会减小文件体积,但通过时会对CPU有额外消耗
3、如果业务场景很看重数据的持久性,那么不应该采用RDB持久化。比如说,如果Redis每5分钟执行一次RDB持久化,要是Redis意外崩溃,则最多会丢失5分钟的数据。
五、Redis持久化方案之AOF
【流程】
Redis执行AOF持久化时,会将接收到的邪命令追加到AOF文件的末尾,因此Redis只要对AOF文件中的命令进行回放,就可以将数据库还原到原先的状态。
与RDB持久化相比,AOF持久化的一个明显优势,它可以提高数据的持久性。因此在AOF模式下,Redis每次接收到Client的写命令,就会命令write()到AOF文件末尾。然而,在Linux中,将数据write()到文件后,数据并不会立刻刷新到磁盘(如果需要将文件内容刷新到磁盘,可以调用fsync() 或 fdatasync() )。
通过appendfsync配置项,可以控制Redis将命令刷新到磁盘的频率:
a、always:每次Redis将命令write()到AOF文件时。都会调用fsync(),将命令刷新到磁盘。这可以保证最好的数据持续性,但却会给系统带来极大的开销。
b、no:Redis只将命令write()到AOF文件。这会让OS决定何时将命令刷新到磁盘。
c、everysec:除了将命令write()到AOF文件,Redis还会每秒执行一次fsync()。在实践中,推荐使用这种设置,一定程度上可以保证数据持久性,又不会明显降低Redis性能。
【问题】
Redis会不断将接受到的写命令追加到AOF文件中,导致AOF文件越来越大。过大的AOF文件会消耗磁盘空间,并且导致Redis重启时更加缓慢。
【办法】
在适当情况下,Redis会对AOF文件进行重写,去除文件中冗余的命令,以减少AOF文件的体积。在重写AOF文件期间,Redis会启动一个子进程,由子进程负者对AOF文件进行重写。可以通过下面两个配置项,控制Redis重写AOF文件的频率:
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 100
上面两个配置的作用:当AOF文件的体积大于64MB,并且AOF文件的体积比上一次大了至少一倍,那么Redis就会执行AOF重写。
【优点】
1、持久化频率高,数据可靠性高
2、没有额外的内存或者CPU消耗
【缺点】
1、文件体积大
2、文件大导致服务数据恢复时效率较低
六、 RDB对比AOF【追加文件】
Redis提供了两种数据持久化的方式,一种是RDB,另外一种是AOF。默认情况下,Redis使用的是RDB持久化。
RDB持久化文件体积较小,但是保存数据的频率一般较低,可靠性差,容易丢失数据。另外RDB写数据时会采用Fork函数拷贝主进程,可能有额外的内存消耗,文件压缩也会有额外的CPU消耗。
AOF持久化可以做到每秒冲持久化一次,可靠性高。但是持久化文件体积较大,导致数据恢复时读取文件时间较长,效率略低。
【小结】RDB和AOF可以同时使用,该情况下,如果Redis重启 ,则优先采用AOF方式恢复数据,因为AOF完整性较好。但是同时开启时服务器只会找AOF文件,所以RDB留作万一的手段。官方也建议双开。
如果没有数据持久化需求,则可以关闭RDB和AOF,Redis就是一个纯内存数据库,类似memcahe。
七、Redis集群方式
Redis集群可以分为主从集群和分片集群。
7.1、主从集群
也叫读写分离集群, 一般一主多从,主库用来写数据,从库用来读数据。结合哨兵,可以在主库宕机时从新选主,目的是保证Redis的高可用。
Redis的复制(replication)功能允许用户根据一个Redis服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。
只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步给从服务器,从而一直保证主从服务器的数据相同。
a、写数据时只能通过主节点完成
b、读数据可以从任何节点完成
c、如果配置了哨兵节点,当master宕机时,哨兵会从slave节点选出一个新的主节点
主从集群分两种:
带有哨兵的集群:
7.2、分片集群
分片集群是数据分片,我们会让多个Redis节点组成集群,并将16383个插槽分到不同的节点上。存储数据时利用对key做hash运算,得到插槽值后存储到对应的节点即可。因为存储数据面向的是插槽而非节点本身,因此可以做到集群动态伸缩。目的是让Redis能存储更多数据。
主从集群中,每个节点都要保存所有信息,容易形成木桶效应。并且当数据量较大时,单个机器无法满足要求。此时我们就要使用分片集群了。
集群特征:
1、每个节点都保存不同数据
2、所有Redis节点彼此互联(PING-PING机制),内部使用二进制协议优化传输速度和带宽
3、节点的fail是通过集群中超过半数的节点检测失效时才生效
4、客户端与Redis节点直连,不需要中间proxy层连接集群中任何一个可用节点都可以访问到数据
5、redis-cluster把所有的物理节点映射到[0-16383]slot(插槽)上,实现动态伸缩
为了保证Redis中每个节点的高可用,我们还可以给每个节点创建replication(slave节点),如下:
出现故障时,主从可以及时切换:
八、Redis的常用数据类型
1、string :最基本的数据类型,二进制安全的字符串,最大512M
2、List :按照添加顺序保持顺序的字符串列表
3、Set :无序的字符串集合,不存在重复的元素
4、sorted set :已排序的字符串集合
5、hash :key-value
九、Redis事务机制
Redis会将一个事务中的所有命令序列化,然后按顺序执行。但是Redis事务不支持回滚操作,命令运行出错后,正确的命令会继续执行。
Redis事务功能是通过MULTI、EXEC、DISCARD、WATCH四个原语实现的。
【MULTI】:用于开启一个事务,它总是返回OK,MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立刻被执行,而是被放到一个带执行命令队列中
【EXEC】:按顺序执行命令队列内的所有命令。返回所有命令的返回值。事务执行过程中,Redis不会执行其它事务的命令。
【DISCARD】:清空命令队列,并放弃执行事务,并且客户端会从事务状态中退出
【WATCH】:Redis乐观锁机制,利用compare-and-set(CAS) 原理,可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行
使用事务时可能会遇到以下两种错误:
1、执行EXEC之前,入队的命令可能会出错。比如,命令可能会产生语法错误(参数数量错误,参数名错误等),或者其他更严重错误,比如内存不足(如果服务器使用maxmemory 设置了最大内存限制的话)。
Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用EXEC命令时,拒绝执行并自动放弃这个事务。
2、命令可能在EXEC调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用宰了字符串键上面,诸如此类。
即使事务中有某个/某些命令在执行时产生了错误,事务中的其他命令仍然会继续执行,不会回滚。
【为何Redis不支持回滚】
以下为不支持回滚的优点:
1、Redis命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
2、因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速。
【小结】
Redis事务其实是把一系列Redis命令放入队列,然后批量执行,执行过程中不会有其他事务来打断。不过与关系型数据库的事务不同,Redis事务不支持回滚操作,事务中某个命令执行失败,其他命令依然会执行。
因此,只要程序员程序编程没问题,理论来说Redis会正确执行所有事务,无序回滚。
【面试点】:如果事务执行一半时候,Redis宕机了如何处理?
【答】:Redis有持久化机制,因为可靠性问题,我们一般用AOF持久化。事务的所有命令也会写入AOF文件,但是如果在执行EXEC命令之前,Redis已经宕机,则AOF文件中事务不完整。使用redis-check-aof程序可以移除AOF文件中不完整事务的信息,确保服务器可以顺利启动。
十、Redis过期策略
10.1、为何需要内存回收
1、在Redis中,set指令可以指定key的过期时间,当过期时间到达以后,key就失效了
2、Redis是基于内存操作的,所有的数据都是保存在内存中,一台机器的内存是有限且很宝贵的
基于以上两点,为了保证Redis能继续提供可靠的服务,Redis需要一种机制清理掉不常用的、无效的、多余的数据,失效后的数据需要及时清理,这就需要内存回收了。
Redis的内存回收主要分为过期删除策略和内存淘汰策略两部分。
10.1.1、过期删除策略
1、【定时删除】
对于每个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就会l立刻删除。该策略可以立刻清楚过期的数据,对内存较好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响时间。
2、【惰性删除】
当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度的节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。
3、【定期删除】
每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。 该策略是前两者的一个折中的方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis中,同事使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取额方式删除部分Key,因此不能保证过期key 100%的删除。
结合定期删除和惰性删除,基本上能很好的处理过期数据的清理,但是实际上还是有点问题的,如果过期key较多,定期删除漏掉的一部分,而且也没有及时去查,即没有走惰性删除,那么就会有大量的过期key堆积在内存中,导致Redis内存耗尽,当内存耗尽后,有新来的key是会直接抛弃还是其他措施呢?有什么办法可以接受更多key?
10.1.2、内存淘汰策略
Redis的内存淘汰策略,指当内存到达maxmemory极限时,使用某种算法来清理掉那些数据,保证新数据的存入。
1、noeviction:当内存不足以容纳写入数据时,新写入操作会报错。
2、allkeys-lru:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,移除最近最少使用key(这个最常用)。
3、allkeys-random:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,随机移除某个key。
4、volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].ecpires)中,移除最近最少使用的key。
5、volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].ecpires)中,随机移除某个key。
6、volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].ecpires)中,有更早过期时间的key优先移除。
注:在配置文件中,通过maxmemory-policy可以配置要使用哪一个淘汰机制。
10.1.3、什么时候进行淘汰
Redis会在每一次处理命令的时候(processCommand函数调用freeMemoryIfNeeded)判断当前Redis是否到达了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的key。
在淘汰key时,Redis默认最常用的事LRU算法(Latest Recently Used)。Redis通过在每一个redisObject保存lru属性来保存key最近的访问时间,在实现LRU算法时直接读取key的lru属性。
具体实现时,Redis遍历每一个db,从每一个db中随机抽取一批样本key,默认是3个key,再从这3个key中,删除最近最少使用的key。
【小结】: Redis过期策略包含定期删除和惰性删除两部分。定期删除是在Redis内部有一个定时任务,会定期删除一些过期的key。惰性删除是当用户查询某个Key时,会检查这个Key是否已经过期,如果没过期则返回用户,如果过期则删除。
但是这两个策略都无法保证过期key一定删除,漏网之鱼越来越多,还可能导致内存溢出。当发生内存不足问题时,Redis还会做内存回收。内存回收采用LRU策略,就是最近最少使用。其原理就是记录每个Key的最近使用时间,内存回收时,随机抽取一些Key,比较其使用时间,把最老的几个删除。
Redis的逻辑是:最近使用过的,很可能再次被使用。
十一、Redis应用场景
1、共享Session
在分布式系统下,服务会部署在不同的tomcat,因此多个tomcat的session无法共享,以前存储在session中的数据无法实现共享,可以用redis代替session,解决分布式系统间数据共享问题。
2、数据缓存
Redis采用内存存储,读写效率较高。我们可以把数据库的访问频率高的热点数据存储到redis中,这样用户请求时优先从redis中读取,减少数据库压力,提高并发能力。
3、异步队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。而且Redis中还有pub/sub这样的专用结构,用于1对N的消息通信模式。
4、分布式锁
Redis中的乐观锁机制,可以帮助我们实现分布式锁的效果,用于解决分布式系统下的多线程安全问题。
十二、缓存击穿、缓存雪崩、缓存穿透
12.1、缓存击穿
【定义】:某一热点数据过期,刚好被超高并发请求,导致数据库压力过大宕机。
【解决办法】:1、使用互斥锁【推荐】。
2、软过期。
12.2、缓存雪崩
【定义】:某段时间内,缓存集中过期,对这批数据的查询压力都落在了数据库上,导致压力过大宕机。
【解决办法】:1、数据分类分批处理:采取不同分类数据,缓存不同周期。
2、相同分类数据:采用固定时长加随机数方式设置缓存。
3、热点数据缓存时间长一些,冷门数据缓存时间短一些。
4、避免redis节点宕机引起雪崩,搭建主从集群,保证高可用。
【小结】:解决缓存雪崩问题的关键是让缓存Key的过期时间分散。因此我们可以把数据按照业务分类,然后设置不同过期时间。相同业务类型的key,设置固定时长加随机数。尽可能保证每个Key的过期时间都不相同。
另外,Redis宕机也可能导致缓存雪崩,因此我们还要搭建Redis主从集群及哨兵监控,保证Redis的高可用。
12.3、缓存穿透
【定义】:大量请求去查询缓存和数据库都不存在的数据,导致数据库压力过大宕机。
【解决办法】:1、缓存空值。
2、BloomFilter(布隆过滤器)。
缓存穿透有两种解决方案:其一是把不存在的key设置null值到缓存中。其二是使用布隆过滤器,在查询缓存前先通过布隆过滤器判断key是否存在,存在再去查询缓存。
设置null值可能被恶意针对,攻击者使用大量不存在的不重复key ,那么方案一就会缓存大量不存在key数据。此时我们还可以对Key规定格式模板,然后对不存在的key做正则规范匹配,如果完全不符合就不用存null值到redis,而是直接返回错误。
Redis使用的是内存存储,当需要海量数据存储时,成本非常高。
经过调研发现,当前主流DDR3内存和主流SATA SSD的单位成本价格差距大概在20倍左右,为了优化redis机器综合成本,我们考虑实现基于热度统计 的数据分级存储及数据在RAM/FLASH之间的动态交换,从而大幅度降低成本,达到性能与成本的高平衡。
基本思路:基于key访问次数(LFU)的热度统计算法识别出热点数据,并将热点数据保留在redis中,对于无访问/访问次数少的数据则转存到SSD上,如果SSD上的key再次变热,则重新将其加载到redis内存中。
目前流行的高性能磁盘存储,并且遵循Redis协议的方案包括:
- SSDB:ssdb.io/zh_cn/
- RocksDB:rocksdb.org.cn/
因此,我们就需要在应用程序与缓存服务之间引入代理,实现Redis和SSD之间的切换,如图:
十四、Redis实现分布式锁
分布式锁需要满足条件:
1、多进程互斥:同一时刻,只有一个进程可以获取锁;
2、保证锁可以释放:任务结束或出现异常,锁一定要释放,避免死锁;
3、阻塞锁(可选):获取锁失败时可否重试;
4、重入锁(可选):获取锁的代码递归调用时,依然可以获取锁。
14.1、最基本的分布式锁
利用Redis的setnx命令,这个命令特征时如果多次执行,只有第一次执行会成功,可以实现互斥的效果。但是为了保证服务宕机时也可以释放锁,需要利用expire命令给锁设置一个有效期。
【问题一】:如果expire之前服务宕机怎么办
【答】:要保证setnx和expire命令的原子性。redis的set命令可以满足:
a、NX:与setnx一致,第一次执行成功
【问题二】:释放锁的时候,如果自己的锁已经过期了,此时会出现安全漏洞,如何解决?
14.2、可重入分布式锁
如果有重入的需求,则除了在锁中记录进程标识,还要记录重试次数,流程如下:
下面我们假设锁的key为“lock
”,hashKey是当前线程的id:“threadId
”,锁自动释放时间假设为20
【获取锁步骤】:
EXISTS lock
-
- 存在,说明有人获取锁了,下面判断是不是自己的锁
- 判断当前线程id作为hashKey是否存在:
HEXISTS lock threadId
- 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end
- 存在,说明是自己获取的锁,重入次数+1:
HINCRBY lock threadId 1
,去到步骤3
- 判断当前线程id作为hashKey是否存在:
- 2、不存在,说明可以获取锁,
HSET key threadId 1
- 3、设置锁自动释放时间,
EXPIRE lock 20
-
- 存在,说明有人获取锁了,下面判断是不是自己的锁
HEXISTS lock threadId
不存在,说明锁已经失效,不用管了
-
-
- 存在,说明锁还在,重入次数减1:
HINCRBY lock threadId -1
,获取新的重入次数
- 存在,说明锁还在,重入次数减1:
- 2、判断重入次数是否为0:
- 为0,说明锁全部释放,删除key:
DEL lock
- 大于0,说明锁还在使用,重置有效时间:
EXPIRE lock 20
- 为0,说明锁全部释放,删除key:
-
14.3、高可用锁
【问题一】:redis分布式锁依赖与redis,如果redis宕机则锁失效。如何解决?
【答】:此时大多数同学会回答说:搭建主从集群,做数据备份。
【问题二】:如果搭建主从集群做数据备份时,进程A获取锁,master还没有把数据备份到slave,master宕机,slave升级为master,此时原来锁失效,其它进程也可以获取锁,出现安全问题。如何解决?
【答】:Redis官网给出了解决方案,使用RedLock思路可以解决:
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
15.1、一致性问题
1、强一致性。这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。
2、弱一致性。这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
3、最终一致性。最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型。
15.2、缓存模式
【旁路缓存模式(Cache-Aside Pattern)】
读流程: 1、读的时候,先读缓存,缓存命中的话,直接返回数据。
2、缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应
写流程:先更新数据库,然后再删除缓存。
【读写穿透(Read-Through/Write-Through)】
该模式,服务端把缓存作为主要数据存储。应用程序更数据库缓存交互,都是通过抽象缓存层完成的。
Read-hrough:1、从缓存读取数据,读到直接返回
2、如果读取不到,从数据库加载,写入缓存后,再返回响应
Read-Through实际只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。
Write-Through:发生请求时,也是在缓存抽象层完成数据源和缓存数据的更新。
【异步缓存写入(Write behind)】
与Read-Through/Write-Though有相似的地方,都是由Cache Provider来负责缓存和数据库读写。它两又有个很大的区别:Read/write Through是同步更新缓存和数据,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。
这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。但是它只适合频繁写的场景。Mysql的InnoDB Buffer Pool机制就是使用这种模式。
15.3、延时双删
1、先删除缓存
2、再更新数据库
3、休眠一会儿(比如1秒),再次删除缓存
小结:休眠时间=读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除请求可能带来的缓存脏数据。
15.4、删除缓存重试机制
不管是三种策略还是延迟双删,都没办法保证一次删除缓存直接成功。就需要引入缓存删除重试机制。
步骤:1、写请求更新数据库
2、缓存因为某些原因,删除失败
3、把删除失败的key放到消息队列
4、消费消息队列的消息,获取要删除的key
5、重试删除缓存操作
整体思路:
MQ生产者,生产了消息之后,通过指定的Topic发送到MQ服务器。然后MQ的消费者,订阅该topic的消息,读取消息数据后,做业务逻辑处理。
1、当用户操作写完数据库,但是删除缓存失败,产生一条MQ消息,发送给MQ服务器。
2、 MQ消费者读取MQ消息,重试5次删除缓存。如果其中有任意一次成功,这返回成功。如果重试5次还是失败,则写入死信队列中。
推荐使用RocketMQ,重试机制和死信队列默认支持。使用起来方便,还支持顺序消息,延迟消息和事务消息等多种业务场景。
删除缓存完全走异步。即写完数据库后不用立刻删除一次缓存。而是直接发MQ消息,到MQ服务器,然后有MQ消费者全权负责删除缓存任务。MQ实时性还是比较高,因此改良后的方案也是一种不错选择。
15.5、binlog
删除缓存重试机制虽然可以,但是会造成很多业务代码入侵。其实还可以通过数据库binlog来异步淘汰key。即读取binlog异步删除缓存。
MySql为例,可以使用阿里的Canal中间件将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致。
1、在业务接口中写数据库之后,就不管了,直接返回成功。
2、Mysql服务器会自动把变更的数据写入Binlog中。
3、Binlog订阅者获取变更的数据,然后删除缓存。
加入重试机制:
在binlog订阅者中如果删除缓存失败,则发送一条mq消息到mq服务器,在mq消费者中自动重试5次。如果有任意一次成功,则直接返回成功。如果重试5次后还是失败,则该消息自动被放入死信队列,后面可能需要人工介入。
十六、参考文章