Redis缓存主要异常及解决方案
作者:京东物流陈昌浩
1简介
Redis是目前最流行的NoSQL数据库。 Redis主要用于缓存。对提高数据查询效率和数据库保护起到关键作用,极大提升系统性能。当然,在使用过程中也可能会出现异常情况,导致Redis失去缓存功能。
2 异常类型
异常主要包括缓存雪崩、缓存穿透和缓存崩溃。
2.1 缓存雪崩
2.1.1 现象
缓存雪崩是指大量请求在缓存中找不到数据而直接访问数据库,导致数据库压力增大,最终导致数据库崩溃数据库崩溃,影响整个系统。用作雪崩。
2.1.2 异常原因
- 缓存服务不可用。
- 缓存服务可用,但大量KEY同时失效。
2.1.3 解决方案
1。缓存服务不可用
redis主要部署方式有单机、主从、耀明、集群模式。
- 一台机器
只有一台机器,所有数据都存储在这台机器上。当机器出现异常时,redis崩溃,会导致redis缓存雪崩。 - 主从
主从实际上是一台机器作为主机,一台或多台机器作为从机。从节点从主节点复制数据,从而可以实现读写分离。主节点写入,从节点读取。
优点:当子节点出现异常时,不会影响使用。
缺点:当主节点异常时,服务将不可用。 - 哨兵
哨兵模式也是主从模式,但是增加了哨兵功能来监控主节点的状态。当父节点宕机时,它会从子节点中投票重新选举出主节点。
优点:高可用性,当主节点异常时,自动在从节点中选择主节点。
缺点:只有一个主节点。当数据量很大时,主节点的压力就会很大。 - 集群模式
集群采用多主多从,按照一定规则分片,数据独立存储,一定程度上解决了哨兵模式下一台机器存储有限的问题。
优点:高可用性,配置多主多从,可以拆分、分散数据,减少一台机器的负载。
缺点:机器资源使用量比较大,配置复杂。 - 总结
从高可用的角度来说,使用哨兵模式和集群模式可以防止redis不可用导致的缓存雪崩问题。
2。大量KEY同时过期
可以设置永不过期,设置不同的过期时间,使用二级缓存,定期更新缓存过期时间
- 设置永不过期
如果所有的key都设置了不失效,就不会出现KEY失效导致的缓存雪崩问题。设置redis key永久有效的命令如下:
PERSIST按钮
缺点:会增加对redis空间资源的要求。 - 设置随机过期时间
如果密钥过期时间不同,则不会同时过期,避免大量数据库访问。
在redis中设置key过期时间的命令如下:
过期key
示例代码如下,通过RedisClient实现
/**
* 随机设置小于30分钟的失效时间
* @param redisKey
* @param value
*/
private void setRandomTimeForReidsKey(String redisKey,String value){
//随机函数
Random rand = new Random();
//随机获取30分钟内(30*60)的随机数
int times = rand.nextInt(1800);
//设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(redisKey,value,times);
}
- 使用二级缓存
使用两组缓冲区中,一级缓存1和二级缓存在一级缓存中,两组缓存中都存储着相同的key,但是它们的过期时间不同。这样,当一级缓存中找不到数据时,可以在二级缓存中查找,而无需直接访问数据库。
示例代码如下:
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从1级缓存中获取数据
String value = test.queryByOneCacheKey("key");
//如果1级缓存中没有数据,再二级缓存中查找
if(StringUtils.isBlank(value)){
value = test.queryBySecondCacheKey("key");
//如果二级缓存中没有,从数据库中查找
if(StringUtils.isBlank(value)){
value =test.getFromDb();
//如果数据库中也没有,就返回空
if(StringUtils.isBlank(value)){
System.out.println("数据不存在!");
}else{
//二级缓存中保存数据
test.secondCacheSave("key",value);
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("数据库中返回数据!");
}
}else{
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("二级缓存中返回数据!");
}
}else {
System.out.println("一级缓存中返回数据!");
}
}
- 异步缓存更新时间
每次访问缓存时,启动一个线程或创建一个异步任务来更新缓存时间。
示例代码如下:
public class CacheRunnable implements Runnable {
private ClusterRedisClientAdapter redisClient;
/**
* 要更新的key
*/
public String key;
public CacheRunnable(String key){
this.key =key;
}
@Override
public void run() {
//更细缓存时间
redisClient.expire(this.getKey(),1800);
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从缓存中获取数据
String value = test.getFromCache("key");
if(StringUtils.isBlank(value)){
//从数据库中获取数据
value = test.getFromDb("key");
//将数据放在缓存中
test.oneCacheSave("key",value);
//返回数据
System.out.println("返回数据");
}else{
//异步任务更新缓存
CacheRunnable runnable = new CacheRunnable("key");
runnable.run();
//返回数据
System.out.println("返回数据");
}
}
3。总结
以上从服务不可用和大面积密钥故障两个方面提出了几种解决方案。上面的代码只是给出了一些思路,具体实现还要考虑。进入现实。当然,还有其他解决方案。我在这里给出的例子使用得更频繁。现实不断变化,没有最好的解决方案,只有最适用的解决方案。
2.2 缓存泄漏
2.2.1 现象
缓存泄漏是指当用户查询一条数据时,数据库和缓存都没有该数据的记录,如果在缓存中找不到该数据,将向数据库发送请求以检索数据。当用户无法获取数据时,就会不断向数据库发送请求和查询,这会给数据库访问带来很大的压力。
2.2.2 异常原因
- 非法呼叫
2.2.3 解决方法
1.非法调用
非法调用导致的缓冲区溢出问题可以通过缓冲空值或者过滤器来解决。
- 空缓存值
如果缓存或数据库中没有值,则可以在缓冲区中存储空值。这样可以减少重复查询空值带来的系统压力的增加,从而优化缓冲区溢出问题。 。
示例代码如下:
private String queryMessager(String key){
//从缓存中获取数据
String message = getFromCache(key);
//如果缓存中没有 从数据库中查找
if(StringUtils.isBlank(message)){
message = getFromDb(key);
//如果数据库中也没有数据 就设置短时间的缓存
if(StringUtils.isBlank(message)){
//设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(key,null,60);
}else{
redisClient.setNxEx(key,message,1800);
}
}
return message;
}
缺点:大量空缓冲区浪费资源,还会导致缓冲区与数据库数据不匹配。
- 布隆过滤器
布隆过滤器是 Bloom 在 1970 年设计的,它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可用于检索元素是否在集合中。这是一种以空间换时间的算法。
布隆过滤器实现的原理是一个非常大的位域和几个哈希函数。
假设哈希函数的数量为3,首先初始化一个位数组,将初始化状态下的维数组的每一位设置为0。如果数据请求的结果为空,则key为按顺序通过三个哈希函数映射。每个映射都会生成一个与位域上的点相对应的哈希值,然后标记相应的位域位置。为1。重新发送数据请求时,使用同样的方法,使用哈希将key映射到位数组上的3个点。如果这三个点中任何一个不为1,则可以认为该键不为空。反之,如果三个点都为1,则KEY一定为空。
缺点:
可能会出现误判,例如A通过哈希函数存储在位置1、3、5。 B 通过哈希函数存储在位置 3、5 和 7 中。 C运行哈希函数得到位置3、5和7。由于3、5和7都有值,因此确定A也在数组中。随着数据的增加,出现这种情况的概率也随之增加。
布隆过滤器无法删除数据。
- 布隆过滤器的改进版本
改进版本将布隆过滤器位图替换为数组。数组中的某个位置映射一次时为+1,删除时为-1。这样就避免了通常用布隆过滤器删除数据后,需要重新计算剩余数据的Hash ,但仍然无法避免错误的判断。 - 布谷鸟过滤器
但如果两个位置都满了,它就得“占鹊巢”,随便踢一个,然后自己占那个位置。与布谷鸟不同,布谷鸟哈希算法将帮助这些受害者(过度拥挤的蛋)找到其他巢穴。由于每个元素可以放在两个位置,只要其中一个位置有空,就可以插入。所以这个悲伤的、被压扁的鸡蛋会检查它的其他位置是否空闲。如果他有空,他就会搬家,每个人都会很高兴。但如果其他人也担任这个职位怎么办?好吧,那他又“接管喜鹊窝”,把受害者的角色转嫁给别人了。然后,这个过程将在新的受害者身上重复,直到所有的卵都找到它们的巢。缺点:
如果场地太拥挤,被踢上百次不停歇,会严重影响插入效率。这个时候,Cuckoo Hash就会设置一个阈值。当连续的巢占用行为超过一定阈值时,阵列被认为接近满。此时,您需要将其展开并移动所有元素。
2。总结
上述方法虽然存在缺点,但可以有效防止大量空数据查询导致的缓冲区溢出问题。除了系统优化外,还需要加强系统监控并发布异常情况。拨打电话后,及时将其加入黑名单。减少异常调用对系统的影响。
2.3 缓冲区分区
2.3.1 现象
key 中存在对应数据。当缓存中key对应的数据过期,并且当时有大量的数据访问请求时,缓存就会过期。请求直接访问数据库并放回到缓存中。高并发访问数据库会导致数据库崩溃。 Redis的高QPS特性可以有效解决数据库查询慢的问题。但是,如果我们系统的并发量很高,在某个时间点缓存突然失效,大量请求进来,那么由于redis不缓存数据,所以我们当时所有的请求都会去查数据库。 。这时候数据库服务就会面临非常高的风险,要么连接满了,要么其他服务不可用。这种情况就是redis缓存崩溃。
2.3.2 异常原因
热点密钥失败时,同时访问大量相同密钥的请求。 ?设置redis key永久有效的命令如下:
PERSIST按钮
缺点:会增加对redis空间资源的要求。
如果密钥过期时间不同,则不会同时过期,避免大量数据库访问。
在redis中设置key过期时间的命令如下:
过期key
示例代码如下,通过RedisClient实现
/**
* 随机设置小于30分钟的失效时间
* @param redisKey
* @param value
*/
private void setRandomTimeForReidsKey(String redisKey,String value){
//随机函数
Random rand = new Random();
//随机获取30分钟内(30*60)的随机数
int times = rand.nextInt(1800);
//设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(redisKey,value,times);
}
- 使用二级缓存
使用两组缓冲区中,一级缓存1和二级缓存在一级缓存中,两组缓存中都存储着相同的key,但是它们的过期时间不同。这样,当一级缓存中找不到数据时,可以在二级缓存中查找,而无需直接访问数据库。
示例代码如下:
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从1级缓存中获取数据
String value = test.queryByOneCacheKey("key");
//如果1级缓存中没有数据,再二级缓存中查找
if(StringUtils.isBlank(value)){
value = test.queryBySecondCacheKey("key");
//如果二级缓存中没有,从数据库中查找
if(StringUtils.isBlank(value)){
value =test.getFromDb();
//如果数据库中也没有,就返回空
if(StringUtils.isBlank(value)){
System.out.println("数据不存在!");
}else{
//二级缓存中保存数据
test.secondCacheSave("key",value);
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("数据库中返回数据!");
}
}else{
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("二级缓存中返回数据!");
}
}else {
System.out.println("一级缓存中返回数据!");
}
}
- 异步缓存更新时间
每次访问缓存时,启动一个线程或创建一个异步任务来更新缓存时间。
示例代码如下:
public class CacheRunnable implements Runnable {
private ClusterRedisClientAdapter redisClient;
/**
* 要更新的key
*/
public String key;
public CacheRunnable(String key){
this.key =key;
}
@Override
public void run() {
//更细缓存时间
redisClient.expire(this.getKey(),1800);
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从缓存中获取数据
String value = test.getFromCache("key");
if(StringUtils.isBlank(value)){
//从数据库中获取数据
value = test.getFromDb("key");
//将数据放在缓存中
test.oneCacheSave("key",value);
//返回数据
System.out.println("返回数据");
}else{
//异步任务更新缓存
CacheRunnable runnable = new CacheRunnable("key");
runnable.run();
//返回数据
System.out.println("返回数据");
}
}
- 分布式锁
使用分布式锁时,一次只能有一个请求访问数据库,其他请求会等待一定时间后再次调用。
示例代码如下:
/**
* 根据key获取数据
* @param key
* @return
* @throws InterruptedException
*/
public String queryForMessage(String key) throws InterruptedException {
//初始化返回结果
String result = StringUtils.EMPTY;
//从缓存中获取数据
result = queryByOneCacheKey(key);
//如果缓存中有数据,直接返回
if(StringUtils.isNotBlank(result)){
return result;
}else{
//获取分布式锁
if(lockByBusiness(key)){
//从数据库中获取数据
result = getFromDb(key);
//如果数据库中有数据,就加在缓存中
if(StringUtils.isNotBlank(result)){
oneCacheSave(key,result);
}
}else {
//如果没有获取到分布式锁,睡眠一下,再接着查询数据
Thread.sleep(500);
return queryForMessage(key);
}
}
return result;
}
2。总结
除了以上解决方案外,还可以提前设置热点数据,利用一些监控手段及时采集热点数据,提前缓存数据。
3 总结
Redis 缓存在互联网上至关重要,可以极大地提高系统效率。本文提供的缓存异常和解决思路可能还不够全面,但也提供了相应的解决思路和通用代码实现。希望它能为您在遇到缓存问题时提供一些解决方案。如果有什么不足的地方,请大家帮忙指出,以便我们共同进步。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。