1.写在前面
前面的博客的简简单单的介绍完了Redis的基本的知识,持久化、API、集群、缓存、分布式锁这些东西,今天的博客的打算将缓存的剩下的几个问题给讲讲完,然后简单的介绍下Redis的事务、发布和订阅、Redis的删除策略和淘汰策略。
2.Redis缓存问题
2.1缓存穿透
缓存穿透:缓存中没有,数据库中也没有,那么不过不做处理,大量的请求就会直接打到数据库上,给数据库造成了很大的压力。
这个前面有一篇博客《Redis入门(二)之缓存穿透》,这篇主要介绍了Redis缓存中的缓存穿透问题,这儿就不做过多的赘述了,我这儿就讲一下具体的解决方案吧。
解决办法:缓存空对象,布隆过滤器
2.2缓存雪崩
缓存雪崩是指机器宕机或在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决办法如下:
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
2.3缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
我们知道,使用缓存,如果获取不到,才会去数据库里获取。但是如果是热点 key,访问量非常的大,数据库在重建缓存的时候,会出现很多线程同时重建的情况。因为高并发导致的大量热点的 key 在重建还没完成的时候,不断被重建缓存的过程,由于大量线程都去做重建缓存工作,导致服务器拖慢的情况。
这儿我就演示下这个效果,还是之前《Redis入门(二)之缓存穿透》中的代码,不过我这儿写了一个测试类,具体的如下:
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.ys.entity.R;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
public class Test {
private static CountDownLatch countDownLatch = new CountDownLatch(99);
@org.junit.Test
public void test() throws InterruptedException {
TicketsRunBle ticketsRunBle = new TicketsRunBle();
for (int i = 0; i < 99; i++) {
Thread thread1 = new Thread(ticketsRunBle, "窗口" + i);
thread1.start();
countDownLatch.countDown();
}
Thread.currentThread().join();
}
public class TicketsRunBle implements Runnable {
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> fastJsonHttpMessageConverters = new ArrayList<>();
fastJsonHttpMessageConverters.add(new FastJsonHttpMessageConverter());
restTemplate.setMessageConverters(fastJsonHttpMessageConverters);
R forObject = restTemplate.getForObject("http://localhost:8080/selectid?id=1", R.class);
System.out.println(forObject);
}
}
}
上面的代码我们用99个线程去访问这个接口,这个时候的索引还没有建立好,就相当于索引失效,理论上这儿数据库只查询一次,然而情况却不是这样的,具体的情况如下:
然后你就发现我们的缓存的机制失效,这就是缓存的击穿,那么我们如何解决呢?加上分布式锁,于是我们修改原来的代码,具体的如下:
public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble, boolean b) {
if (!bloomFilter.isExist(key)) {
return new R().setCode(600).setData(new NullValueResultDO()).setMsg("非法访问");
}
//查询缓存
Object redisObj = valueOperations.get(String.valueOf(key));
//命中缓存
if (redisObj != null) {
//正常返回数据
return new R().setCode(200).setData(redisObj).setMsg("OK");
}
redisLock.lock(key);
try{
//查询缓存
redisObj = valueOperations.get(String.valueOf(key));
if (redisObj != null) {
//正常返回数据
return new R().setCode(200).setData(redisObj).setMsg("OK");
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock(key);
}
T load = cacheLoadble.load();//查询数据库
if (load != null) {
valueOperations.set(key, load, expire, unit); //加入缓存
return new R().setCode(200).setData(load).setMsg("OK");
}
return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果");
}
上面的代码主要是加上了一个Redis的分布式锁,在查询缓存为空的时候,然后加锁,再查一次缓存,然后如果查不到的话,就再走数据库,然后启动我们的项目,然后再来测试看看我们的加的Redis分布式的效果如何?具体的如下:
发现这儿就查询了一次数据库,这样我们的问题就解决了。
3.Redis的事务
3.1什么是Redis的事务
Redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体,就是一个队列,当执行的时候,一次性按照添加顺序依次执行,中间不会被打断或者干扰。
3.2用来干什么?
一个队列中,一次性,顺序性,排他性的执行一系列命令。
3.3Redis事务基本的操作
- 开启事务:multi 设置事务开始位置,这个指令开启后,后面所有的指令都会加入事务中。
- 执行事务:exec 设置事务的结束位置,同时执行事务,与multi成对出现,成对使用。
- 取消事务:discard 终止当前事务,取消multi后,exec前的所有命令。
- 注意:加入事务的命令并没有立马执行,而且加入队列中,exec命令后才执行。
3.4加入和执行事务有错误会怎么办?
加入事务语法错误,事务则取消。**(全体连坐)**具体的如下:
执行事务报错,则成功的返回成功,失败的返回失败,不会影响报错后面的指令。**(冤有头,债有主)**具体的如下:
注意:已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己实现回滚。
3.5监控key
- watch:对key进行监控,如果在exec执行前,监控的key发生了变化,终止事务执行。具体的如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LdeiNBVZ-1626421764300)(/Users/king/博客/Redis/img5/5.png)]
- unwatch:取消对所有的key进行监控
4.Redis发布订阅
- publish:发布消息 语法:publish channel名称 “消息内容”
- subscribe:订阅消息 语法:subscribe channel名称
- psubscribe:使用通配符订阅消息 语法:pubscribe channel*名称
- punsubscribe:使用统配符退订消息 语法:punsubscribe channel*名称
- unsubscribe:退订消息 语法:unsubscribe channel名称
5.删除策略
5.1定时删除
先画个图吧,然后再讲具体的逻辑,具体的如下:
上面的就是先将键值对存入Redis中,然后Redis有一块内存存的是这些值的地址和存入Redis的时间戳,Redis中会开启一个定时器,定时扫描这些值有没有过期,如果过期的话,就直接删除。
以CPU换取Redis内存。
5.2惰性删除
就是所有的内容都不删除,而是等查询的时候,查看这个键是不是过期了,如果过期,就再删除。如果没过期,就直接返回对应的值。这样做就会有很多无效的数据。
以Redis内存换取CPU
5.3定期删除
- Redis在启动的时候读取配置文件hz的值,默认为10
- 每秒执行hz次serverCron()–>databasesCron()–>actveExpireCyle()
- actveExpireCyle()对每个expires[*]进行逐一检测,每次执行250ms/hz
- 对某个expires[*]检测时,随机挑选N个key检查
- 如果key超时,删除key
- 如果一轮中删除的key的数量大于N*25%,循环该过程
- 如果一轮中删除的key的数量小于等于N*25%,检查下一个expires[*]
- current_db用于记录actveExpireCyle()进入哪个expires[*]执行,如果时间到了,那么下次根据current_db继续执行。
注意:Redis中默认的是惰性删除和定期删除
6.淘汰策略
就是Redis中内存满了,Redis该怎么处理的这些数据,以及怎么处理添加的新数据。
相关配置:
# 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,
# 当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。
# Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes>
#当内存使用达到最大值时,redis使用的清除策略。有以下几种可以选择(明明有6种,官方配置文件里却说有5种可以选择?):
# volatile针对的是加了过期时间的数据, allkeys针对的是所有的数据
# 1)volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used ) 最近最少使用
# 2)allkeys-lru 利用LRU算法移除任何key
# 3)volatile-random 移除设置过过期时间的随机key
# 4)allkeys-random 移除随机key
# 5)volatile-ttl 移除即将过期的key(minor TTL)
# 6)noeviction 不移除任何key,只是返回一个写错误 。默认选项
maxmemory-policy noeviction
# LRU 和 minimal TTL 算法都不是精准的算法,但是相对精确的算法(为了节省内存),随意你可以选择样本大小进行检测。redis默认选择5个样本进行检测,你可以通过maxmemory-samples进行设置样本数。
maxmemory-samples 5
7.写在最后
本篇博客主要简简单单的介绍了下Redis的三大缓存问题、Redis事务、Redis发布订阅、Redis中的删除策略、Redis中内存满了的淘汰的策略。