一.cache aside pattern 先删除缓存再修改数据库
?? 为什么是删除不是更新
1.先更新缓存而后修改数据库,若修改数据库失败(网络故障),就导致缓存与数据库数据不一致
2. 很多时候,复杂点的缓存场景,缓存中的数据不单单是从数据库取出来的值,可能还需要其他表查询一些数据,然后进行一些运算,才能算出值是多少
3.更新缓存的代价是很高的,举个例子,一个缓存涉及表的字段,在1分钟内就修改了20次,或者100次。那么缓存更新就要更新20次,100次,有大量的冷数据
删除缓存而不是更新缓存是懒汉式的思想,不是每次都做复杂的计算,而是当他需要被使用的时候才进行
OK:先搞清楚服务啊,要不然就乱了,有单个的服务,就是更新库存服务和删除库存服务,这都是库存服务,删除服务是调用rediscluster对外提供的接口实现的。(ProductInventoryService)
步骤:
1.线程池+内存队列初始化
2.两种请求对象封装
3.请求异步执行Service封装
4.两种请求Controller接口封装
5.读请求去重化
6.空数据度请求过滤优化
代码实现:
/**
解惑:RequestProcessorThreadPool中创建了一个固定线程的线程池,循环线程数量的for去创建同样个数的队列,将队列放入队列集合中(RequestQueue,同样是单例,里面提供了addQueue方法)
并利用线程池准备分配线程,但线程分配需要task(RequestProcessorThread 它继承了Callable接口),里面有成员变量去接受Queue内存队列,
然后执行里面的call方法
**/
(1)在项目启动的时候利用监听器创建一个线程池(线程池中线程的数量由配置文件指定)(单例)
(2)创建一个内存队列集合(单例)内存队列的个数和线程的个数相等。一个内存队列中有一个阻塞的队列(ArrayBlockingQueue有界队列、
LinkedBlokuingQueue无界队列)。队列里元素的类型是Request请求(请求里面有方法,如读写操作(读写操作都实现这个接口,里面肯定有ID),)
request是一个接口,不管是查询服务还是更新服务都实现了这个接口,里面有商品信息和库存服务,调用的时候可以在new时直接有参初始化。
写请求(InventoryCntDBUpdateRequest调用库存对外提供服务,删除缓存(redisDAO),更新数据)
(3)先单独写出两个服务,之间的关系不考虑,(不是不考虑关系,而是这两个是一起进行的,属于一次写请求,肯定是一起执行的,这里不用考虑多线
程,因为在队列里,对商品的一次的写操作肯定只有它自己在执行,所以不用考虑线程安全)更新服务,根据传入的商品对象,删除redis中的缓存,然后
更新数据库。
读请求
(1)从数据库中查询最新的商品库存数量
(2)将最新的商品库存数量,刷新到redis缓存中请求异步执行Service作用:商品请求的路由以及优化
请求异步执行Service
(1)做请求的路由,根据每个请求的商品的id,路由到对应的内存队列中去
//先获取productId的hash值
String key = String.valueof(productId);
int h;
int hash = (key == null) ? 0 : (h = nkey.hashCode()) ^ (h >>> 16);
//对hash值取模,将hash值路由到指定的内存队列中
int index = (requestQueue.queueSize() - 1)& hash
通过index获取内存队列,将请求放入内存队列中 queue.put(request)
Request request = queue.take();
request.process();
(2)获取路由到的内存队列
//根据index从List<ArrayBlockingQueue<Request>>中获取ArrayBlockingQueue
(3)RequestProcessorThread 请求处理线程
它监控一个内存队列
执行里面的call方法,无限循环,不断的从队列中去消费请求
两种请求Controller接口封装
(1)更新商品库存请求
主要是两步 1. 执行更新服务(删除缓存,更换数据库)2.异步执行Service,将请求发送过去,往队列里面放入(错误: 都执行了还放什么?)
2. new 一个更新服务对象,及写请求,异步执行Service,将请求发送过去,往队列里面放入
代码: Response response =null;
try{
Request request = new ProductInventoryDBUpdateRequest(
productInventory, productInventoryService);
requestAsyncProcessService.process(request);
response = new Response(Response.SUCCESS);
}catch(Exception e){
e.printStackTrace();
response = new Response(Response.FAILURE);
}
return response;
(2) 读取商品库存请求(Controller)
1. new 一个库存读取 ,异步执行Service,将请求发送过去,往队列里面放入
*** 将请求扔给Service异步处理后,就需要在这轮循 while(true)一会,这这里hang住,去尝试等待前面有商品库存更新的操作
try{
Request request = new ProductInventoryCacheRefreshRequest(
productId, productInventoryService, false);
requestAsyncProcessService.process(request);
//等待设定时间
long startTime = System.currentTimeMillis();
long endTime = 0L;
long waitTime = 0L;
while(true){
if(waitTime > 200){
break;
}
//尝试去redis中读取一次商品库存的缓存数据
//如果读取到了数据,那么就返回,如果没有读取到,那么就等待一段时间
if(proudctInventory != null){
return productInventory;
}else{
Thread.sleep(20);
endTime = System.currentTimeMillis();
waitTime = endTime - startTime;
}
}
//循环结束,仍没有跳出方法,说明没有从缓存中获取数据,这个时候可以考虑直接读取数据库中的数据
productInventory = productInventoryService.findProductInventory(productId);
if(productInventory != null) {
return productInventory;
}
}catch(Exception e ){
e.printStackTrace();
}
//若走到这,说明等待了200ms仍没有从缓存中获取到数据,返回存储对面,数量为-1
return return new ProductInventory(productId, -1L);
//读缓存时: 代码如下(productInventoryService.getProductInventoryCache)
Long inventoryCnt = 0L;
if(result != null && "".equals(result)){
try{
//****为什么要放在try catch块里面? 因为读取的库存数量可能是乱的字符串
inventoryCnt = Long.valueOf(result);
result = new ProductInventory(productId,inventoryCnt);
}catch(Exception e){
e.printStackTrace(0;)
}
return null;
}
代码优化::读请求去重
在RequestQueue中(内存队列中:?里面有说明方法,这个类实现了Callable接口,里面有Call方法)
为什么会这样理解?我认为内存队列中集合里面每一个队列都应有一个ConcurrentHashMap,但在多线程的,这样也可以,没错。
他将ConcurrentHashMap放在了内存队列集合中,也可以。
在RequestProcessorThread中,对从内存队列集合中取出来的值进行判断
if(request instanceof RroductInventoryDBUpdateRequest){
//如果请求是一个更新请求,那么将productId对应的标记设置为true
Map<Integer,Boolean> map = requestQueue.getMap();
}else if(request instanceof ProductInventoryCacheRefreshRequest){
//如果是缓存刷新的请求,那么就判断,如果标识不为空而是true;
Boolean flag = map.get(request.getProductId());
if(flag != null && flag == true){
flagMap.put(flag.put(request.getProductId(),false));
}
//如果是缓存刷新的请求,发现表示不为空,但是标识是false
说明前面已经有了一个数据库更新请求+一个数据库缓存请求,或之前已经被读取过了
if(flaf != null && !flag){
return;
}
//这种情况解决的是读请求刚过来,发现标识为null,就路由到队列,读数据库,写缓存,然后后面的读请求又过来了,继续读数据库,写缓存,所以这里应该设为false;
if(flag==null){
flagMap.put(flag.put(request.getProductId(),false));
}
}
创建一个vo类 里面是请求的响应(Response)
public static final String SUCCESS="seccess";
public static final String DAILURE="failure"
private String status;
private String message;
对上面方案中的一些BUG进行修正
1.去请求去重不不能在request请求路由之前
???能有什么问题?
同一个商品,获取了同一个ConcurrentHashMap,一个读一个写。可能会有一些问题,放在Queue取出请求那里更好。
2.执行完一个读请求之后,假设数据已经更新到redis中了,但是后面的redis中的数据会因为内存满了,被自动清理掉,清理掉之后,又来了一个读请求,这个时候就一直读取数据库,写缓存
3.********错:(如果 之前是false,但里面没有读请求,那么之后的读请求会在Controller里面一直的hang住,到200ms之后发送刷新缓存的请求,但是,刷新缓存的请求也是读请求,直接会被过滤掉。)
不是因为它被过滤掉,它是直接掉用,没有在队列中,可能还是会有缓存与数据库数据不一致的问题。
???不清楚?代码到要读取数据库刷新缓存的时候,只有三种情况。
1.就是说,上一次也是读请求,数据刷入了redis,但是redis LRU算法给清理掉了,标志
位还是false,所以此时下一个读请求在内存中拿不到数据,再放一个读request进队列,让数据去刷新一下。
2.如果在200ms里面,就是读请求一直积压着,没有等到它执行,(在生产环境中就比较坑了,你需要去扩容机器了),所有就直接查一次数据库,然后给队列塞进去一个刷新缓存的请求(但这个请求不会执行)
3.数据库本身就没有,就会涉及到缓存穿透,请求直接到达MySQL
在getCache中设置一个标志,强制刷新,默认是false