redis系列之数据库与缓存数据一致性解决方案

本文探讨了缓存与数据库更新策略,特别是在高并发场景下的数据一致性问题。提出了使用队列来处理更新请求的方法,有效避免了数据不一致的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据库与缓存读写模式策略

写完数据库后是否需要马上更新缓存还是直接删除缓存?
(1)、如果写数据库的值与更新到缓存值是一样的,不需要经过任何的计算,可以马上更新缓存,但是如果对于那种写数据频繁而读数据少的场景并不合适这种解决方案,因为也许还没有查询就被删除或修改了,这样会浪费时间和资源
(2)、如果写数据库的值与更新缓存的值不一致,写入缓存中的数据需要经过几个表的关联计算后得到的结果插入缓存中,那就没有必要马上更新缓存,只有删除缓存即可,等到查询的时候在去把计算后得到的结果插入到缓存中即可。
所以一般的策略是当更新数据时,先删除缓存数据,然后更新数据库,而不是更新缓存,等要查询的时候才把最新的数据更新到缓存

数据库与缓存双写情况下导致数据不一致问题

场景一

当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。

场景一解决方案


这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。

场景二

在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况

场景二解决方案

遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。

在高并发下解决场景二要注意的问题
(1)读请求时长阻塞
 由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时间内返回,该解决方案最大的风险在于可能数据更新很频繁,导致队列中挤压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,像遇到这种情况,一般要做好足够的压力测试,如果压力过大,需要根据实际情况添加机器。
(2)请求并发量过高
 这里还是要做好压力测试,多模拟真实场景,并发量在最高的时候QPS多少,扛不住就要多加机器,还有就是做好读写比例是多少
(3)多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上
(4)热点商品的路由问题,导致请求的倾斜
某些商品的读请求特别高,全部打到了相同的机器的相同丢列里了,可能造成某台服务器压力过大,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是很大,但是确实有可能某些服务器的负载会高一些。

数据库与缓存数据一致性解决方案流程图


数据库与缓存数据一致性解决方案对应代码

商品库存实体
[java]  view plain  copy
  1. package com.shux.inventory.entity;  
  2. /** 
  3.  ********************************************** 
  4.  *  描述: 
  5.  * Simba.Hua 
  6.  * 2017年8月30日 
  7.  ********************************************** 
  8. **/  
  9. public class InventoryProduct {  
  10.     private Integer productId;  
  11.     private Long InventoryCnt;  
  12.       
  13.     public Integer getProductId() {  
  14.         return productId;  
  15.     }  
  16.     public void setProductId(Integer productId) {  
  17.         this.productId = productId;  
  18.     }  
  19.     public Long getInventoryCnt() {  
  20.         return InventoryCnt;  
  21.     }  
  22.     public void setInventoryCnt(Long inventoryCnt) {  
  23.         InventoryCnt = inventoryCnt;  
  24.     }  
  25.       
  26. }  

请求接口
[java]  view plain  copy
  1. /** 
  2.  ********************************************** 
  3.  *  描述: 
  4.  * Simba.Hua 
  5.  * 2017年8月27日 
  6.  ********************************************** 
  7. **/  
  8. public interface Request {  
  9.     public void process();  
  10.     public Integer getProductId();  
  11.     public boolean isForceFefresh();  
  12. }  
数据更新请求
[java]  view plain  copy
  1. package com.shux.inventory.request;  
  2.   
  3. import org.springframework.transaction.annotation.Transactional;  
  4.   
  5. import com.shux.inventory.biz.InventoryProductBiz;  
  6. import com.shux.inventory.entity.InventoryProduct;  
  7.   
  8. /** 
  9.  ********************************************** 
  10.  *  描述:更新库存信息 
  11.  *  1、先删除缓存中的数据 
  12.  *  2、更新数据库中的数据 
  13.  * Simba.Hua 
  14.  * 2017年8月30日 
  15.  ********************************************** 
  16. **/  
  17. public class InventoryUpdateDBRequest implements Request{  
  18.     private InventoryProductBiz inventoryProductBiz;  
  19.     private InventoryProduct inventoryProduct;  
  20.       
  21.     public InventoryUpdateDBRequest(InventoryProduct inventoryProduct,InventoryProductBiz inventoryProductBiz){  
  22.         this.inventoryProduct = inventoryProduct;  
  23.         this.inventoryProductBiz = inventoryProductBiz;  
  24.     }  
  25.     @Override  
  26.     @Transactional  
  27.     public void process() {  
  28.         inventoryProductBiz.removeInventoryProductCache(inventoryProduct.getProductId());  
  29.         inventoryProductBiz.updateInventoryProduct(inventoryProduct);  
  30.     }  
  31.     @Override  
  32.     public Integer getProductId() {  
  33.         // TODO Auto-generated method stub  
  34.         return inventoryProduct.getProductId();  
  35.     }  
  36.     @Override  
  37.     public boolean isForceFefresh() {  
  38.         // TODO Auto-generated method stub  
  39.         return false;  
  40.     }  
  41.   
  42. }  

查询请求
[java]  view plain  copy
  1. package com.shux.inventory.request;  
  2.   
  3. import com.shux.inventory.biz.InventoryProductBiz;  
  4. import com.shux.inventory.entity.InventoryProduct;  
  5.   
  6. /** 
  7.  ********************************************** 
  8.  *  描述:查询缓存数据 
  9.  *  1、从数据库中查询 
  10.  *  2、从数据库中查询后插入到缓存中 
  11.  * Simba.Hua 
  12.  * 2017年8月30日 
  13.  ********************************************** 
  14. **/  
  15. public class InventoryQueryCacheRequest implements Request {  
  16.     private InventoryProductBiz inventoryProductBiz;  
  17.     private Integer productId;  
  18.     private boolean isForceFefresh;  
  19.       
  20.     public InventoryQueryCacheRequest(Integer productId,InventoryProductBiz inventoryProductBiz,boolean isForceFefresh) {  
  21.         this.productId = productId;  
  22.         this.inventoryProductBiz = inventoryProductBiz;  
  23.         this.isForceFefresh = isForceFefresh;  
  24.     }  
  25.     @Override  
  26.     public void process() {  
  27.       InventoryProduct  inventoryProduct = inventoryProductBiz.loadInventoryProductByProductId(productId);  
  28.       inventoryProductBiz.setInventoryProductCache(inventoryProduct);  
  29.     }  
  30.     @Override  
  31.     public Integer getProductId() {  
  32.         // TODO Auto-generated method stub  
  33.         return productId;  
  34.     }  
  35.     public boolean isForceFefresh() {  
  36.         return isForceFefresh;  
  37.     }  
  38.     public void setForceFefresh(boolean isForceFefresh) {  
  39.         this.isForceFefresh = isForceFefresh;  
  40.     }  
  41.       
  42.   
  43. }  

spring启动时初始化队列线程池
[java]  view plain  copy
  1. package com.shux.inventory.thread;  
  2.   
  3. import java.util.concurrent.ArrayBlockingQueue;  
  4. import java.util.concurrent.ExecutorService;  
  5. import java.util.concurrent.Executors;  
  6.   
  7. import com.shux.inventory.request.Request;  
  8. import com.shux.inventory.request.RequestQueue;  
  9. import com.shux.utils.other.SysConfigUtil;  
  10.   
  11. /** 
  12.  ********************************************** 
  13.  *  描述:请求处理线程池,初始化队列数及每个队列最多能处理的数量 
  14.  * Simba.Hua 
  15.  * 2017年8月27日 
  16.  ********************************************** 
  17. **/  
  18. public class RequestProcessorThreadPool {  
  19.     private static final int blockingQueueNum = SysConfigUtil.get("request.blockingqueue.number")==null?10:Integer.valueOf(SysConfigUtil.get("request.blockingqueue.number").toString());  
  20.     private static final int queueDataNum = SysConfigUtil.get("request.everyqueue.data.length")==null?100:Integer.valueOf(SysConfigUtil.get("request.everyqueue.data.length").toString());  
  21.     private ExecutorService threadPool = Executors.newFixedThreadPool(blockingQueueNum);  
  22.     private RequestProcessorThreadPool(){  
  23.         for(int i=0;i<blockingQueueNum;i++){//初始化队列  
  24.             ArrayBlockingQueue<Request> queue = new ArrayBlockingQueue<Request>(queueDataNum);//每个队列中放100条数据  
  25.             RequestQueue.getInstance().addQueue(queue);  
  26.             threadPool.submit(new RequestProcessorThread(queue));//把每个queue交个线程去处理,线程会处理每个queue中的数据  
  27.         }  
  28.     }  
  29.     public static class Singleton{  
  30.        private static RequestProcessorThreadPool instance;  
  31.        static{  
  32.            instance = new RequestProcessorThreadPool();  
  33.        }  
  34.        public static RequestProcessorThreadPool getInstance(){  
  35.            return instance;  
  36.        }  
  37.     }  
  38.     public static RequestProcessorThreadPool getInstance(){  
  39.         return Singleton.getInstance();  
  40.     }  
  41.     /** 
  42.      * 初始化线程池 
  43.      */  
  44.     public static void init(){  
  45.         getInstance();  
  46.     }  
  47. }  

请求处理线程
[java]  view plain  copy
  1. package com.shux.inventory.thread;  
  2.   
  3. import java.util.Map;  
  4. import java.util.concurrent.ArrayBlockingQueue;  
  5. import java.util.concurrent.Callable;  
  6.   
  7. import com.shux.inventory.request.InventoryUpdateDBRequest;  
  8. import com.shux.inventory.request.Request;  
  9. import com.shux.inventory.request.RequestQueue;  
  10.   
  11. /** 
  12.  ********************************************** 
  13.  *  描述:请求处理线程 
  14.  * Simba.Hua 
  15.  * 2017年8月27日 
  16.  ********************************************** 
  17. **/  
  18. public class RequestProcessorThread implements Callable<Boolean>{  
  19.     private ArrayBlockingQueue<Request> queue;  
  20.     public RequestProcessorThread(ArrayBlockingQueue<Request> queue){  
  21.         this.queue = queue;  
  22.     }  
  23.     @Override  
  24.     public Boolean call() throws Exception {  
  25.        Request request = queue.take();  
  26.        Map<Integer,Boolean> flagMap = RequestQueue.getInstance().getFlagMap();  
  27.        //不需要强制刷新的时候,查询请求去重处理  
  28.            if (!request.isForceFefresh()){  
  29.                if (request instanceof InventoryUpdateDBRequest) {//如果是更新请求,那就置为false  
  30.                    flagMap.put(request.getProductId(), true);  
  31.                } else {  
  32.                    Boolean flag = flagMap.get(request.getProductId());  
  33.                    /** 
  34.                     * 标志位为空,有三种情况 
  35.                     * 1、没有过更新请求 
  36.                     * 2、没有查询请求 
  37.                     * 3、数据库中根本没有数据 
  38.                     * 在最初情况,一旦库存了插入了数据,那就好会在缓存中也会放一份数据, 
  39.                     * 但这种情况下有可能由于redis中内存满了,redis通过LRU算法把这个商品给清除了,导致缓存中没有数据 
  40.                     * 所以当标志位为空的时候,需要从数据库重查询一次,并且把标志位置为false,以便后面的请求能够从缓存中取 
  41.                     */  
  42.                    if ( flag == null) {  
  43.                        flagMap.put(request.getProductId(), false);  
  44.                    }  
  45.                    /** 
  46.                     * 如果不为空,并且flag为true,说明之前有一次更新请求,说明缓存中没有数据了(更新缓存会先删除缓存), 
  47.                     * 这个时候就要去刷新缓存,即从数据库中查询一次,并把标志位设置为false 
  48.                     */  
  49.                    if ( flag != null && flag) {  
  50.                        flagMap.put(request.getProductId(), false);  
  51.                    }  
  52.                    /** 
  53.                     * 这种情况说明之前有一个查询请求,并且把数据刷新到了缓存中,所以这时候就不用去刷新缓存了,直接返回就可以了 
  54.                     */  
  55.                    if (flag != null && !flag) {  
  56.                        flagMap.put(request.getProductId(), false);  
  57.                        return true;  
  58.                    }   
  59.                }  
  60.            }  
  61.            request.process();  
  62.         return true;  
  63.     }  
  64.       
  65. }  

请求队列
[java]  view plain  copy
  1. package com.shux.inventory.request;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5. import java.util.Map;  
  6. import java.util.concurrent.ArrayBlockingQueue;  
  7. import java.util.concurrent.ConcurrentHashMap;  
  8.   
  9. /** 
  10.  ********************************************** 
  11.  *  描述:请求队列 
  12.  * Simba.Hua 
  13.  * 2017年8月27日 
  14.  ********************************************** 
  15. **/  
  16. public class RequestQueue {  
  17.     private List<ArrayBlockingQueue<Request>> queues = new ArrayList<>();  
  18.       
  19.     private Map<Integer,Boolean> flagMap = new ConcurrentHashMap<>();  
  20.     private RequestQueue(){  
  21.           
  22.     }  
  23.     private static class Singleton{  
  24.         private static RequestQueue queue;  
  25.         static{  
  26.             queue = new RequestQueue();  
  27.         }  
  28.         public static RequestQueue getInstance() {  
  29.             return queue;  
  30.         }  
  31.     }  
  32.       
  33.     public static RequestQueue getInstance(){  
  34.         return Singleton.getInstance();  
  35.     }  
  36.     public void addQueue(ArrayBlockingQueue<Request> queue) {  
  37.         queues.add(queue);  
  38.     }  
  39.       
  40.     public int getQueueSize(){  
  41.         return queues.size();  
  42.     }  
  43.     public ArrayBlockingQueue<Request> getQueueByIndex(int index) {  
  44.         return queues.get(index);  
  45.     }  
  46.       
  47.     public Map<Integer,Boolean> getFlagMap() {  
  48.         return this.flagMap;  
  49.     }  
  50. }  


spring 启动初始化线程池类

[java]  view plain  copy
  1. package com.shux.inventory.listener;  
  2.   
  3. import org.springframework.context.ApplicationListener;  
  4. import org.springframework.context.event.ContextRefreshedEvent;  
  5.   
  6. import com.shux.inventory.thread.RequestProcessorThreadPool;  
  7.   
  8. /** 
  9.  ********************************************** 
  10.  *  描述:spring 启动初始化线程池类 
  11.  * Simba.Hua 
  12.  * 2017年8月27日 
  13.  ********************************************** 
  14. **/  
  15. public class InitListener implements  ApplicationListener<ContextRefreshedEvent>{  
  16.   
  17.     @Override  
  18.     public void onApplicationEvent(ContextRefreshedEvent event) {  
  19.         // TODO Auto-generated method stub  
  20.         if(event.getApplicationContext().getParent() != null){  
  21.             return;  
  22.         }  
  23.         RequestProcessorThreadPool.init();  
  24.     }  
  25. }  
异步处理请求接口
[java]  view plain  copy
  1. package com.shux.inventory.biz;  
  2.   
  3. import com.shux.inventory.request.Request;  
  4.   
  5. /** 
  6.  ********************************************** 
  7.  *  描述:请求异步处理接口,用于路由队列并把请求加入到队列中 
  8.  * Simba.Hua 
  9.  * 2017年8月30日 
  10.  ********************************************** 
  11. **/  
  12. public interface IRequestAsyncProcessBiz {  
  13.     void process(Request request);  
  14. }  

异步处理请求接口实现
[java]  view plain  copy
  1. package com.shux.inventory.biz.impl;  
  2.   
  3. import java.util.concurrent.ArrayBlockingQueue;  
  4.   
  5. import org.slf4j.Logger;  
  6. import org.slf4j.LoggerFactory;  
  7. import org.springframework.stereotype.Service;  
  8.   
  9. import com.shux.inventory.biz.IRequestAsyncProcessBiz;  
  10. import com.shux.inventory.request.Request;  
  11. import com.shux.inventory.request.RequestQueue;  
  12.   
  13.   
  14. /** 
  15.  ********************************************** 
  16.  *  描述:异步处理请求,用于路由队列并把请求加入到队列中 
  17.  * Simba.Hua 
  18.  * 2017年8月30日 
  19.  ********************************************** 
  20. **/  
  21. @Service("requestAsyncProcessService")  
  22. public class RequestAsyncProcessBizImpl implements IRequestAsyncProcessBiz {  
  23.     private Logger logger = LoggerFactory.getLogger(getClass());  
  24.     @Override  
  25.     public void process(Request request) {  
  26.         // 做请求的路由,根据productId路由到对应的队列  
  27.         ArrayBlockingQueue<Request> queue = getQueueByProductId(request.getProductId());  
  28.         try {  
  29.             queue.put(request);  
  30.         } catch (InterruptedException e) {  
  31.             logger.error("产品ID{}加入队列失败",request.getProductId(),e);  
  32.         }  
  33.     }  
  34.   
  35.     private ArrayBlockingQueue<Request> getQueueByProductId(Integer productId) {  
  36.         RequestQueue  requestQueue = RequestQueue.getInstance();  
  37.         String key = String.valueOf(productId);  
  38.         int hashcode;  
  39.         int hash = (key == null) ? 0 : (hashcode = key.hashCode())^(hashcode >>> 16);  
  40.         //对hashcode取摸  
  41.         int index = (requestQueue.getQueueSize()-1) & hash;  
  42.         return requestQueue.getQueueByIndex(index);  
  43.     }  
  44.       
  45.    
  46.   
  47. }  


数据更新请求controller
[java]  view plain  copy
  1. package com.shux.inventory.controller;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.stereotype.Controller;  
  5. import org.springframework.web.bind.annotation.RequestMapping;  
  6. import org.springframework.web.bind.annotation.ResponseBody;  
  7.   
  8. import com.shux.inventory.biz.IRequestAsyncProcessBiz;  
  9. import com.shux.inventory.biz.InventoryProductBiz;  
  10. import com.shux.inventory.entity.InventoryProduct;  
  11. import com.shux.inventory.request.InventoryUpdateDBRequest;  
  12. import com.shux.inventory.request.Request;  
  13. import com.shux.utils.other.Response;  
  14.   
  15. /** 
  16.  ********************************************** 
  17.  *  描述:提交更新请求 
  18.  * Simba.Hua 
  19.  * 2017年9月1日 
  20.  ********************************************** 
  21. **/  
  22. @Controller("/inventory")  
  23. public class InventoryUpdateDBController {  
  24.     private @Autowired InventoryProductBiz inventoryProductBiz;  
  25.     private @Autowired IRequestAsyncProcessBiz requestAsyncProcessBiz;  
  26.     @RequestMapping("/updateDBInventoryProduct")  
  27.     @ResponseBody  
  28.     public Response updateDBInventoryProduct(InventoryProduct  inventoryProduct){  
  29.         Request request = new InventoryUpdateDBRequest(inventoryProduct,inventoryProductBiz);  
  30.         requestAsyncProcessBiz.process(request);  
  31.        return new Response(Response.SUCCESS,"更新成功");  
  32.     }  
  33. }  
数据查询请求controller
[java]  view plain  copy
  1. package com.shux.inventory.controller;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.stereotype.Controller;  
  5. import org.springframework.web.bind.annotation.RequestMapping;  
  6.   
  7. import com.shux.inventory.biz.IRequestAsyncProcessBiz;  
  8. import com.shux.inventory.biz.InventoryProductBiz;  
  9. import com.shux.inventory.entity.InventoryProduct;  
  10. import com.shux.inventory.request.InventoryQueryCacheRequest;  
  11. import com.shux.inventory.request.Request;  
  12.   
  13. /** 
  14.  ********************************************** 
  15.  *  描述:提交查询请求 
  16.  *  1、先从缓存中取数据 
  17.  *  2、如果能从缓存中取到数据,则返回 
  18.  *  3、如果不能从缓存取到数据,则等待20毫秒,然后再次去数据,直到200毫秒,如果超过200毫秒还不能取到数据,则从数据库中取,并强制刷新缓存数据 
  19.  * Simba.Hua 
  20.  * 2017年9月1日 
  21.  ********************************************** 
  22. **/  
  23. @Controller("/inventory")  
  24. public class InventoryQueryCacheController {  
  25.     private @Autowired InventoryProductBiz inventoryProductBiz;  
  26.     private @Autowired IRequestAsyncProcessBiz requestAsyncProcessBiz;  
  27.     @RequestMapping("/queryInventoryProduct")  
  28.     public InventoryProduct queryInventoryProduct(Integer productId) {  
  29.          Request request = new InventoryQueryCacheRequest(productId,inventoryProductBiz,false);  
  30.          requestAsyncProcessBiz.process(request);//加入到队列中  
  31.          long startTime = System.currentTimeMillis();  
  32.          long allTime = 0L;  
  33.          long endTime = 0L;  
  34.          InventoryProduct inventoryProduct = null;  
  35.          while (true) {  
  36.              if (allTime > 200){//如果超过了200ms,那就直接退出,然后从数据库中查询  
  37.                  break;  
  38.              }  
  39.              try {  
  40.                  inventoryProduct = inventoryProductBiz.loadInventoryProductCache(productId);  
  41.                  if (inventoryProduct != null) {  
  42.                      return inventoryProduct;  
  43.                  } else {  
  44.                      Thread.sleep(20);//如果查询不到就等20毫秒  
  45.                  }   
  46.                  endTime = System.currentTimeMillis();  
  47.                  allTime = endTime - startTime;  
  48.              } catch (Exception e) {  
  49.              }   
  50.          }  
  51.          /** 
  52.           * 代码执行到这来,只有以下三种情况 
  53.           * 1、缓存中本来有数据,由于redis内存满了,redis通过LRU算法清除了缓存,导致数据没有了 
  54.           * 2、由于之前数据库查询比较慢或者内存太小处理不过来队列中的数据,导致队列里挤压了很多的数据,所以一直没有从数据库中获取数据然后插入到缓存中 
  55.           * 3、数据库中根本没有这样的数据,这种情况叫数据穿透,一旦别人知道这个商品没有,如果一直执行查询,就会一直查询数据库,如果过多,那么有可能会导致数据库瘫痪 
  56.           */  
  57.          inventoryProduct = inventoryProductBiz.loadInventoryProductByProductId(productId);  
  58.          if (inventoryProduct != null) {  
  59.              Request forcRrequest = new InventoryQueryCacheRequest(productId,inventoryProductBiz,true);  
  60.              requestAsyncProcessBiz.process(forcRrequest);//这个时候需要强制刷新数据库,使缓存中有数据  
  61.              return inventoryProduct;  
  62.          }  
  63.          return null;  
  64.            
  65.      }  
  66. }  



转载。 https://blog.youkuaiyun.com/simba_1986/article/details/77823309
<think>嗯,用户问的是Redis数据库的数据一致性怎么解决。这个问题应该是在使用缓存时经常遇到的,我得先理清楚常见的场景和问题。首先,用户可能是在开发中用了Redis作为缓存层,但更新数据库的时候,缓存的数据可能没及时更新,导致不一致的情况。比如,用户更新了数据库的信息,但Redis里还是旧数据,这样其他用户读取的时候就会看到过时的信息。 接下来,我需要回想一下常见的解决方案。首先想到的是缓存更新策略,比如Cache-Aside模式,也就是先更新数据库,再删除缓存。不过有时候可能会有并发问题,比如当缓存失效时,多个线程同时去读数据库然后写缓存,这时候可能会有脏数据。这时候可能需要加锁或者用其他机制来避免。 然后还有Read-Through和Write-Through模式,这些模式里应用程序直接读写缓存,缓存自己负责和数据库同步。这可能对代码侵入性小一些,但需要缓存支持这样的功能,比如某些缓存库或者框架提供的功能。 另外还有Write-Behind模式,也就是异步更新数据库,这样写缓存很快,但风险是如果缓存挂了,数据可能会丢失。这个需要根据业务场景来决定是否适用,比如对数据持久性要求不高的场景。 然后,数据同步策略方面,延迟双删可能是一个办法,就是在更新数据库前后都删除缓存,中间加个延迟,确保读请求完成后再删一次。不过这延迟时间怎么设置是个问题,太短可能没效果,太长又影响性能。另外,可能需要用消息队列来异步处理,或者用数据库的binlog来同步,比如通过Canal监听binlog变化,然后更新缓存,这样保证顺序性。 还有,事务机制方面,如果数据库Redis支持事务,可能可以一起提交,但实际中Redis的事务并不是强一致的,可能会有问题。所以通常还是用最终一致性,而不是强一致。 另外需要考虑的是缓存过期时间,设置合理的TTL,即使出现不一致,过一段时间缓存自动失效,重新从数据库加载,这可以作为一种兜底策略。不过对于实时性要求高的场景可能不够。 用户可能的业务场景不同,解决方案也不同。比如,对一致性要求高的,可能需要更复杂的策略,比如分布式锁或者版本号控制。而对于允许短暂不一致的,可能用简单的延迟删除或者异步更新就够了。 还需要注意缓存穿透、雪崩等问题,但这些可能不是用户当前的问题重点。用户主要关心一致性,所以应该围绕这个展开。 总结一下,可能的解决步骤是:先确定缓存更新策略(如Cache-Aside),处理并发情况,使用双删或异步消息,利用数据库日志同步,设置合理的过期时间,以及根据业务需求选择强一致或最终一致。同时,需要分析具体业务场景,比如读多写少还是写多读少,数据的重要性等,来选择最合适的方案。 可能还需要提到一些实际应用中的问题,比如删除缓存失败怎么办,这时候可能需要重试机制,或者引入消息队列确保删除操作最终执行。另外,如果缓存删除后,大量请求同时打到数据库,导致缓存击穿,这里可以用互斥锁或者缓存预热的方法来避免。 总之,解决Redis数据库一致性问题需要结合多种策略,根据具体情况选择合适的方法,并且理解每种方法的优缺点,比如性能损耗、实现复杂度、数据一致性强度等。</think>### Redis数据库数据一致性解决方案分步解析 --- #### 一、问题本质 Redis作为缓存层数据库(如MySQL)存在数据同步延迟,核心矛盾在于: - **缓存更新时机**:数据库更新后,缓存何时失效或更新? - **并发读写冲突**:多个线程/服务同时读写可能引发脏数据。 --- #### 二、核心解决策略 ##### 1. **缓存更新模式** | 模式 | 实现方式 | 优点 | 缺点 | 适用场景 | |------|----------|------|------|----------| | **Cache-Aside** | 读时加载缓存,更新时先写库再删缓存 | 简单可控 | 需处理并发问题 | 通用场景 | | **Read/Write-Through** | 缓存作为代理,自动同步数据库 | 业务无感知 | 依赖缓存组件支持 | 框架集成场景 | | **Write-Behind** | 先更新缓存,异步批量写库 | 写入性能高 | 数据丢失风险 | 写入频繁且容忍延迟的场景 | **代码示例(Cache-Aside写操作):** ```python def update_data(key, value): # 1. 更新数据库 db.update(key, value) # 2. 删除缓存 redis.delete(key) ``` --- ##### 2. **并发问题解决方案** - **延迟双删策略**(针对缓存删除后旧数据回写问题): ```python def update_data(key, value): # 1. 首次删除缓存 redis.delete(key) # 2. 更新数据库 db.update(key, value) # 3. 延迟后再删一次(如500ms) time.sleep(0.5) redis.delete(key) ``` - **互斥锁(Mutex Lock)**: ```python def get_data(key): data = redis.get(key) if data is None: # 加锁防止并发击穿 if lock.acquire(): try: data = db.query(key) # 查数据库 redis.set(key, data) # 写缓存 finally: lock.release() return data ``` --- ##### 3. **数据同步机制** - **基于数据库日志(Binlog)**: - 工具:Canal/Aliyun DTS监听数据库Binlog - 流程:`DB更新 → Binlog解析 → 更新/删除Redis` - 特点:**严格顺序执行,最终一致性** - **消息队列异步通知**: ```python def update_data(key, value): db.update(key, value) # 写数据库 mq.send("cache_delete", key) # 发送消息 # 消费者侧 def mq_consumer(): while True: key = mq.receive() redis.delete(key) # 删除缓存 ``` --- ##### 4. **兜底设计** - **设置缓存过期时间**:$$TTL = \text{max}(业务容忍时间, 30\text{s})$$ - **版本号控制**: ```redis SET user:123 "{ver:5, name:张三}" ``` 更新时校验版本号,防止旧数据覆盖。 --- #### 三、方案选型建议 | 场景 | 推荐策略 | 一致性级别 | 实现复杂度 | |------|----------|------------|------------| | **读多写少** | Cache-Aside + 延迟双删 | 最终一致 | 低 | | **写多读少** | Write-Behind + 异步队列 | 最终一致 | 中 | | **强一致性要求** | 直连数据库(禁用缓存) | 强一致 | 低(性能差) | | **高并发读** | Binlog同步 + 版本号 | 最终一致 | 高 | --- #### 四、注意事项 1. **权衡一致性需求**:强一致性需牺牲性能,90%场景最终一致即可。 2. **监控重试**:缓存操作失败需记录日志并重试(如通过消息队列)。 3. **压测验证**:模拟高并发场景测试策略有效性。 --- 通过组合以上策略,可有效缓解Redis数据库的数据一致性问题,具体需根据业务特点选择平衡点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值