数据库与缓存双写不一致保障方案

 一.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

猜你喜欢

转载自blog.csdn.net/qq_40280705/article/details/82292319