缓存类型
客户端缓存
对于BS架构的互联网应用来说客户端缓存主要分为页面缓存和浏览器缓存两种,对于APP而言主要是自身所使用的缓存。
清理缓存的方式:
以网页浏览器为例
1.使用Ctrl+F5可以强制刷新浏览器本地缓存
2.浏览器中的参数设置的时间已过期如:Expires,Cache-control: max-age, Last-Modified标签等
网络中缓存
网络中的缓存主要是指代理服务器对客户端请求数据的缓存,主要分为WEB代理缓存和边缘缓存(CDN边缘缓存)
反向代理缓存
基本介绍
反向代理位于应用服务器机房,处理所有对WEB服务器的请求。
如果用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户。如果没有缓冲则先向WEB服务器发出请求,取回数据,本地缓存后再发送给用户。通过降低向WEB服务器的请求数,从而降低了WEB服务器的负载。
应用场景
一般只缓存体积较小静态文件资源,如css、js、图片
开源实现
主要为nginx
服务端缓存
1.堆缓存
堆缓存:使用Java堆内存来存储缓存对象,位于服务本机的内存中
优点:没有序列化/反序列化,省去网络传输过程是最快的缓存;
缺点:
1)缓存数据量有限,受限于堆空间大小,一般存软应用/弱引用对象即当堆空间不足时GC会强制回收释放空间,数据同步较麻烦;
2)单服务是集群部署的时候,应该考虑是否需要做集群中本地缓存的数据同步
常用堆缓存Guava 、Map等
2.堆外缓存
堆外缓存:缓存对象存储在堆外,应用中的缓存组件,应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销;
优点:更大容量的缓存空间,减少GC扫描和移动对象;
缺点:读取数据时需要序列化,速度比堆内缓存慢,数据同步较麻烦;
常用堆缓存Ehcahe等
EhCache
特点:
EhCache是进程内的缓存框架,在集群时不推荐使用
EhCache在第一次通过select查询出的结果被加入到EhCache缓存中,第二次查询从EhCache取出的对象与第一次查询对象实际上是同一个对象,因此我们在更新age的时候,实际已经更新了EhCache中的缓存对象。
故EhCache自带刷新缓存能力
3.分布式缓存
分布式缓存:分布式缓存由一个服务端实现管理和控制,有一个或多个客户端节点存储数据的缓存;
优点:具有较大容量的缓存空间,易于扩容;相对于堆(外)缓存、磁盘缓存更容易保证数据一致性;
缺点:需要序列化/反序列化及网络传输
常用的分布式缓存 Redis Memcached
4.数据库缓存
数据库在设计的时候也有缓存操作,更改相关参数开启查询缓存
什么是CDN
CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。
CDN的优势:
(1)CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
(2)大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。
引入CDN后的网路架构如图:
客户端浏览器先检查是否有本地缓存是否过期,如果过期,则向CDN边缘节点发起请求,CDN边缘节点会检测用户请求数据的缓存是否过期,如果没有过期,则直接响应用户请求,此时一个完成http请求结束;如果数据已经过期,那么CDN还需要向源站发出回源请求(back to the source request),来拉取最新的数据。CDN的典型拓扑图如下:
CDN缓存
浏览器本地缓存失效后,浏览器会向CDN边缘节点发起请求。类似浏览器缓存,CDN边缘节点也存在着一套缓存机制。
CDN缓存的缺点
CDN的分流作用不仅减少了用户的访问延时,也减少的源站的负载。但其缺点也很明显:当网站更新时,如果CDN节点上数据没有及时更新,即便用户再浏览器使用Ctrl +F5的方式使浏览器端的缓存失效,也会因为CDN边缘节点没有同步最新数据而导致用户访问异常。
CDN缓存策略
CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的Cache-control: max-age的字段来设置CDN边缘节点数据缓存时间。
当客户端向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN服务商一般会提供基于文件后缀、目录多个维度来指定CDN缓存时间,为用户提供更精细化的缓存管理。
CDN缓存时间会对“回源率”产生直接的影响。若CDN缓存时间较短,CDN边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;若CDN缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。
CDN缓存刷新
CDN边缘节点对开发者是透明的,相比于浏览器Ctrl+F5的强制刷新来使浏览器本地缓存失效,开发者可以通过CDN服务商提供的“刷新缓存”接口来达到清理CDN边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制CDN节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。
更多CDN相关技术请参考:
1.CDN学习笔记一(CDN是什么?)https://www.cnblogs.com/tinywan/p/6067126.html
一、Redis
Redis 简介
Redis本质上是一个Key-Value类型的内存数据库,它将整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作.
Redis 与其他 key - value 缓存产品有以下三个特点:
1.Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次从磁盘中加载数据进行使用。可以对部分数据加入内存中,下次访问则无需访问数据库,直接访问内存即可,极大的加快了访问速度,对于热点商品,并发量大的极有意义
持久化的过程(具体方式待续):
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。
2.Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3.Redis支持数据的备份,即master-slave模式的数据备份
缺点:
1.Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
2.耗内存,当redis内存达到一定容量时,读写性能会下降
查看redis内存使用情况命令(待续)
redis使用场景
1.做缓存:
冷热隔离:即热点数据和并发量不大的数据
2.分布式session
3.分布式锁
4.队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 结构,这使得Redis能作为一个很好的消息队列平台来使用。
5.发布/订阅????
分布式环境下为何使用redis?
主要解决的是性能和并发,还可以使用redis实现分布式锁。
可参考:为什么分布式一定要有redis? https://www.cnblogs.com/bigben0123/p/9115597.html
二。Redis 数据类型
1.String(字符串)
string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value,但一个字符串类型的值能存储最大容量是512M。string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。一般做一些复杂的计数功能的缓存。
实例
redis 127.0.0.1:6379> SET name "w3cschool.cn"
OK
redis 127.0.0.1:6379> GET name
"w3cschool.cn"
2.Set(集合)
Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。
如:
redis 127.0.0.1:6379> sadd w3cschool.cn rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd w3cschool.cn rabitmq
(integer) 0
Redis DEL 命令用于删除已存在的键。不存在的 key 会被忽略。
如果键被删除成功,命令执行后输出 (integer) 1,否则将输出 (integer) 0
Redis Smembers 命令返回集合中的所有的成员。 不存在的集合 key 被视为空集合。如:
redis 127.0.0.1:6379> SADD myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SADD myset1 "world"
(integer) 1
redis 127.0.0.1:6379> SMEMBERS myset1
1) "World"
2) "Hello"
3.sorted set有序集合)
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。
4.列表(List)
List数据结构是链表结构,是双向的,可以在链表左,右两边分别操作;
使用List的数据结构,可以做简单的消息队列的功能(既可以保证消息的顺序性)。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
5.哈希(Hash)
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。如在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
Redis 事务
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
一个事务从开始到执行会经历以下三个阶段:
开始事务。
命令入队。
执行事务。
redis 线程模型
分析:这个问题其实是对redis内部机制的一个考察。很多人其实都不知道redis是单线程工作模型。所以,这个问题还是应该要复习一下的。
回答:主要是以下三点
(一)纯内存操作
(二)单线程操作,避免了频繁的上下文切换
(三)采用了非阻塞I/O多路复用机制
redis线程模型,如图所示
简单来说,就是。我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/0多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
需要说明的是,这个I/O多路复用机制,redis还提供了select、epoll、evport、kqueue等多路复用函数库,大家可以自行去了解。
I/O多路复用机制请参考: https://www.cnblogs.com/dolphin0520/p/3916526.html
redis架构演进
单机版三个问题:
1、 内存容量有限
2、处理能力有限
3、无法高可用。
2.Redis 多机版
主要有三种方案:1、复制( Replication) 2、哨兵(Sentinel) 3、集群(Cluster)。
Redis 多机版特性功能:
复制:扩展系统对于读的能力
哨兵:为服务器提供高可用特性,减少故障停机出现
集群:扩展内存容量,增加机器,提高性能读写能力和存储以及提供高可用特性
1.主从Redis
Redis 的复制( replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器( master),而通过复制创建出来的服务器复制品则为从服务器( slave)。 只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同
其架构图如下:
优点:
降低 master 读压力,讲读压力转交给了从库
缺点:
1.人为关注Master是否宕机
2.无法完成自动切换主从
3.没有解决 master 写的压力
2.哨兵
当主数据库遇到异常中断服务后,开发者可以通过手动的方式选择一个从数据库来升格为主数据库,以使得系统能够继续提供服务。然而整个过程相对麻烦且需要人工介入,难以实现自动化。 为此,Redis 2.8开始提供了哨兵工具来实现自动化的系统监控和故障恢复功能。 哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。
顾名思义,哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。
(1)监控主数据库和从数据库是否正常运行。
(2)主数据库出现故障时自动将从数据库转换为主数据库。
其模型如图:
Master与slave的切换过程:
(1)slave leader升级为master
(2)其他slave修改为新master的slave
(3)客户端修改连接
(4)老的master如果重启成功,变为新master的slave
集群
redis-cluster集群
即使使用哨兵,redis每个实例也是全量存储,每个redis存储的内容都是完整的数据,浪费内存且有木桶效应。为了最大化利用内存,可以采用cluster群集,就是分布式存储。即每台redis存储不同的内容。
采用redis-cluster架构正是满足这种分布式存储要求的集群的一种体现。redis-cluster架构中,被设计成共有16384个hash slot。每个master分得一部分slot,其算法为:hash_slot = crc16(key) mod 16384 ,这就找到对应slot。采用hash slot的算法,实际上是解决了redis-cluster架构下,有多个master节点的时候,数据如何分布到这些节点上去。key是可用key,如果有{}则取{}内的作为可用key,否则整个可以是可用key。群集至少需要3主3从,且每个实例使用不同的配置文件。
其架构如图所示:
redis-cluster集群的高可用
在redis-cluster架构中,redis-master节点一般用于接收读写,而redis-slave节点则一般只用于备份,其与对应的master拥有相同的slot集合,若某个redis-master意外失效,则再将其对应的slave进行升级为临时redis-master。
节点分区
数据分区方案
1.哈希取余分区
哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。
该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
2.一致性哈希分区
一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器
优点:与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。
缺点:一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。
3.带虚拟节点的一致性哈希分区
该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小,实际只会对槽进行重新分配。仍以上图为例,系统中有4个实际节点,假设为其分配16个槽(0-15); 槽0-3位于node1,4-7位于node2,以此类推。如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽6分配给node3,槽7分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。
槽的数量一般远小于2^32,远大于实际节点的数量;在Redis集群中,槽的数量为16384。
下面这张图很好的总结了Redis集群将数据映射到实际节点的过程:
(1)Redis对数据的特征值(一般是key)计算哈希值,使用的算法是CRC16。
(2)根据哈希值,计算数据属于哪个槽。使用算法:哈希值对16384取模
(3)根据槽与节点的映射关系,计算数据属于哪个节点。
分区的原理是采用带虚拟节点的一致性哈希分区
一致性哈希相关的原理请参考: 程序员小灰-漫画:什么是一致性哈希?https://blog.csdn.net/xaccpJ2EE/article/details/82993120?utm_source=blogxgwz7
客户端访问集群
edis-cli
(1)计算key属于哪个槽:CRC16(key) & 16383
(2)判断key所在的槽是否在当前节点:假设key位于第i个槽,clusterState.slots[i]则指向了槽所在的节点,如果clusterState.slots[i]==clusterState.myself,说明槽在当前节点,可以直接在当前节点执行命令;
否则,说明槽不在当前节点,则查询槽所在节点的地址(clusterState.slots[i].ip/port),并将其包装到MOVED错误中返回给redis-cli。
(3)redis-cli收到MOVED错误后,根据返回的ip和port重新发送请求。
具体例子:请参考:深入学习Redis(5):集群https://www.cnblogs.com/kismetv/p/9853040.html
缓存使用
缓存命中率:
缓存命中率是从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。
缓存命中率 = 从缓存中读取次数 / {总读取次数 (从缓存中读取次数 +从慢设备上读取次数) }
这个指标非常重要,直接影响到系统性能,也可以通过该指标来衡量缓存是否运行良好;
缓存带来的问题:
缓存穿透
即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
(一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。其实质就是限制流量,类似的方法还有使用消息队列进行流量消峰。
(二)采用异步更新策略,无论key是否取到值,都直接返回。这样就不用请求数据库了。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
但这样对用户来说获取数据就直接变成了从缓存获取,适合访问频率级高的.
(三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。合法的话才进入缓存判断。
缓存雪崩
即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决办法:
(一)给缓存的失效时间,加上一个随机值,避免集体失效。即在设置过期时间时,让所配置的过期时间在乘以一个随机值
(二)使用互斥锁,但是该方案吞吐量明显下降了。即流量消峰
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
1)从缓存A读数据库,有则直接返回
2)A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
3)更新线程同时更新缓存A和缓存B。
但这样会增大内存空间。缓存B可以使用lru算法来兼容空间问题。
缓存击穿
指的是并发量很高的 KEY,在该 KEY 失效的瞬间有很多请求同同时去请求数据库,更新缓存。例如我们有一个业务 KEY,该 KEY 的并发请求量为 10000。当该 KEY 失效的时候,就会有 1 万个线程会去请求数据库更新缓存。这个时候如果没有采取适当的措施,那么数据库很可能崩溃。
解决办法:
1.使用互斥锁
当从缓存获取的是null值时,其他key 处于等待现象,必须等待第一个构建完缓存之后,释放锁,其他人才能通过该key才能访问数据;那其他人就直接从缓存里面取数据,不会造成数据库的读写性能的缺陷;
原理如下图:
如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。
分布式版源码如下:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
// 3 min timeout to avoid mutex holder crash
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
这里的“永远不过期”包含两层意思:
其实现原理如下:
源码如下:
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
四种解决方案:没有最佳只有最合适
如何解决redis的并发竞争key问题
分析:这个问题大致就是,同时有多个子系统去set一个key。可能会导致的问题:数据库和缓存的数据不一致。
回答:如下所示
(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
其他方法,比如利用队列,将set方法变成串行访问也可以。总之,灵活变通
Redis相比memcached有哪些优势?
(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
(2) redis可以持久化其数据
redis的过期策略以及内存淘汰机制
分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?
回答:
redis采用的是定期删除+惰性删除策略。
redis过期策略
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除:redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
惰性删除:于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
内存淘汰机制
在redis.conf中有一行配置
maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己),redis默认的策略是volatile-lru。
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
redis和数据库双写一致性问题
分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列的重试机制
分析:
将不一致分为三种情况:
数据库有数据,缓存没有数据;
数据库有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据。
读请求:
不要求强一致性的读请求,走redis,要求强一致性的直接从mysql读取。读操作优先读取redis,不存在的话就去访问MySQL,并把读到的数据写回Redis中;
写请求:
数据首先都写到数据库,然后把缓存里对应的数据失效掉(删掉)。(先写redis再写mysql,如果写入失败事务回滚会造成redis中存在脏数据)
思考:为何是删缓存而不是更新缓存?
这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。
1)而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。
2)A更新成功了,B更新失败了。则缓存里是A的数据。这样缓存和数据库的数据也不一致。
删除缓存失败的补偿方案参考:
对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。(个人理解当删除缓存失败后,应该还要为当前key值设置标记,当下一次对当前key值有查询请求时,直接读数据库,直到删除失败的标记消失为止)
定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
给所有的缓存一个失效期。
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
并发情况:
并发不高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写: 写mysql->成功,再写redis;
并发高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;
场景分析:
1.在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,
还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,此时缓存的值为100.原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况
参考方案:
遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据(不要更新缓存),一般情况下是可以取到的。
参考:
1.Redis怎么保持缓存与数据库一致性? https://blog.csdn.net/belalds/article/details/82078009
2.Redis 如何保持和MySQL数据一致 https://blog.csdn.net/thousa_ho/article/details/78900563
多级缓存
所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。我们应用的整体架构如图所示:
整体流程如上图所示:
1)首先接入Nginx将请求负载均衡到应用Nginx,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用Nginx的缓存命中率,相对于轮询,一致性哈希会存在单机热点问题,一种解决办法是热点直接推送到接入层Nginx,一种办法是设置一个阀值,当超过阀值,改为轮询算法。
2)接着应用Nginx读取本地缓存(本地缓存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盘/内存)、Local Redis实现),如果本地缓存命中则直接返回,使用应用Nginx本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点问题非常有效。
3)如果Nginx本地缓存没命中,则会读取相应的分布式缓存(如Redis缓存,另外可以考虑使用主从架构来提升性能和吞吐量),如果分布式缓存命中则直接返回相应数据(并回写到Nginx本地缓存)。
4)如果分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。
5)在Tomcat应用中,首先读取本地堆缓存,如果有则直接返回(并会写到主Redis集群),为什么要加一层本地堆缓存将在缓存崩溃与快速修复部分细聊。
6)作为可选部分,如果步骤4没有命中可以再尝试一次读主Redis集群操作,。目的是防止当从有问题时的流量冲击。
7)如果所有缓存都没有命中只能查询DB或相关服务获取相关数据并返回。
8)步骤7返回的数据异步写到主Redis集群,此处可能多个Tomcat实例同时写主Redis集群,可能造成数据错乱,如何解决该问题将在更新缓存与原子性部分细聊。
应用整体分了三部分缓存:应用Nginx本地缓存、分布式缓存、Tomcat堆缓存,每一层缓存都用来解决相关的问题,如应用Nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少访问回源率、Tomcat堆缓存用于防止相关缓存失效/崩溃之后的冲击。
参考资料:
1.为什么分布式一定要有redis? https://www.cnblogs.com/bigben0123/p/9115597.html
2.redis 五种数据结构详解(string,list,set,zset,hash)https://www.cnblogs.com/sdgf/p/6244937.html
3.redis架构演变与redis-cluster群集读写方案 https://yq.aliyun.com/articles/625912?utm_content=m_1000013190
4.程序员小灰-漫画:什么是一致性哈希?https://blog.csdn.net/xaccpJ2EE/article/details/82993120?utm_source=blogxgwz7
5.深入学习Redis(5):集群https://www.cnblogs.com/kismetv/p/9853040.html
6.缓存穿透,缓存击穿,缓存雪崩解决方案分析https://blog.csdn.net/zeb_perfect/article/details/54135506
7.深入理解分布式系统中的缓存架构(上) https://blog.csdn.net/yelvgou9995/article/details/81079463
8.《深入分布式缓存》之“亿级请求下多级缓存那些事 https://blog.csdn.net/wireless_com/article/details/79134166