redis系列之數據庫與緩存數據一致性解決方案
數據庫與緩存讀寫模式策略
寫完數據庫后是否需要馬上更新緩存還是直接刪除緩存?
(1)、如果寫數據庫的值與更新到緩存值是一樣的,不需要經過任何的計算,可以馬上更新緩存,但是如果對於那種寫數據頻繁而讀數據少的場景並不合適這種解決方案,因為也許還沒有查詢就被刪除或修改了,這樣會浪費時間和資源
(2)、如果寫數據庫的值與更新緩存的值不一致,寫入緩存中的數據需要經過幾個表的關聯計算后得到的結果插入緩存中,那就沒有必要馬上更新緩存,只有刪除緩存即可,等到查詢的時候在去把計算后得到的結果插入到緩存中即可。
所以一般的策略是當更新數據時,先刪除緩存數據,然后更新數據庫,而不是更新緩存,等要查詢的時候才把最新的數據更新到緩存
數據庫與緩存雙寫情況下導致數據不一致問題
場景一
當更新數據時,如更新某商品的庫存,當前商品的庫存是100,現在要更新為99,先更新數據庫更改成99,然后刪除緩存,發現刪除緩存失敗了,這意味着數據庫存的是99,而緩存是100,這導致數據庫和緩存不一致。
場景一解決方案
這種情況應該是先刪除緩存,然后在更新數據庫,如果刪除緩存失敗,那就不要更新數據庫,如果說刪除緩存成功,而更新數據庫失敗,那查詢的時候只是從數據庫里查了舊的數據而已,這樣就能保持數據庫與緩存的一致性。
場景二
在高並發的情況下,如果當刪除完緩存的時候,這時去更新數據庫,但還沒有更新完,另外一個請求來查詢數據,發現緩存里沒有,就去數據庫里查,還是以上面商品庫存為例,如果數據庫中產品的庫存是100,那么查詢到的庫存是100,然后插入緩存,插入完緩存后,原來那個更新數據庫的線程把數據庫更新為了99,導致數據庫與緩存不一致的情況
場景二解決方案
遇到這種情況,可以用隊列的去解決這個問,創建幾個隊列,如20個,根據商品的ID去做hash值,然后對隊列個數取摸,當有數據更新請求時,先把它丟到隊列里去,當更新完后在從隊列里去除,如果在更新的過程中,遇到以上場景,先去緩存里看下有沒有數據,如果沒有,可以先去隊列里看是否有相同商品ID在做更新,如果有也把查詢的請求發送到隊列里去,然后同步等待緩存更新完成。
這里有一個優化點,如果發現隊列里有一個查詢請求了,那么就不要放新的查詢操作進去了,用一個while(true)循環去查詢緩存,循環個200MS左右,如果緩存里還沒有則直接取數據庫的舊數據,一般情況下是可以取到的。
在高並發下解決場景二要注意的問題
(1)讀請求時長阻塞
由於讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時間內返回,該解決方案最大的風險在於可能數據更新很頻繁,導致隊列中擠壓了大量的更新操作在里面,然后讀請求會發生大量的超時,最后導致大量的請求直接走數據庫,像遇到這種情況,一般要做好足夠的壓力測試,如果壓力過大,需要根據實際情況添加機器。
(2)請求並發量過高
這里還是要做好壓力測試,多模擬真實場景,並發量在最高的時候QPS多少,扛不住就要多加機器,還有就是做好讀寫比例是多少
(3)多服務實例部署的請求路由
可能這個服務部署了多個實例,那么必須保證說,執行數據更新操作,以及執行緩存更新操作的請求,都通過nginx服務器路由到相同的服務實例上
(4)熱點商品的路由問題,導致請求的傾斜
某些商品的讀請求特別高,全部打到了相同的機器的相同丟列里了,可能造成某台服務器壓力過大,因為只有在商品數據更新的時候才會清空緩存,然后才會導致讀寫並發,所以更新頻率不是太高的話,這個問題的影響並不是很大,但是確實有可能某些服務器的負載會高一些。
數據庫與緩存數據一致性解決方案流程圖
數據庫與緩存數據一致性解決方案對應代碼
商品庫存實體
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 } 27
請求接口
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 }
數據更新請求
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 }
查詢請求
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啟動時初始化隊列線程池
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 }
請求處理線程
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 }
請求隊列
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 啟動初始化線程池類
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 }
異步處理請求接口
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 } 15
異步處理請求接口實現
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 }
1 package com.shux.inventory.biz.impl; 2 3 import javax.annotation.Resource; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.stereotype.Service; 7 8 import com.shux.inventory.biz.InventoryProductBiz; 9 import com.shux.inventory.entity.InventoryProduct; 10 import com.shux.inventory.mapper.InventoryProductMapper; 11 import com.shux.redis.biz.IRedisBiz; 12 13 /** 14 ********************************************** 15 * 描述 16 * Simba.Hua 17 * 2017年8月30日 18 ********************************************** 19 **/ 20 @Service("inventoryProductBiz") 21 public class InventoryProductBizImpl implements InventoryProductBiz { 22 private @Autowired IRedisBiz<InventoryProduct> redisBiz; 23 private @Resource InventoryProductMapper mapper; 24 @Override 25 public void updateInventoryProduct(InventoryProduct inventoryProduct) { 26 // TODO Auto-generated method stub 27 mapper.updateInventoryProduct(inventoryProduct); 28 } 29 30 @Override 31 public InventoryProduct loadInventoryProductByProductId(Integer productId) { 32 // TODO Auto-generated method stub 33 return mapper.loadInventoryProductByProductId(productId); 34 } 35 36 @Override 37 public void setInventoryProductCache(InventoryProduct inventoryProduct) { 38 redisBiz.set("inventoryProduct:"+inventoryProduct.getProductId(), inventoryProduct); 39 40 } 41 42 @Override 43 public void removeInventoryProductCache(Integer productId) { 44 redisBiz.delete("inventoryProduct:"+productId); 45 46 } 47 48 @Override 49 public InventoryProduct loadInventoryProductCache(Integer productId) { 50 // TODO Auto-generated method stub 51 return redisBiz.get("inventoryProduct:"+productId); 52 } 53 54 }
數據更新請求controller
1 package com.shux.inventory.biz.impl; 2 3 import javax.annotation.Resource; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.stereotype.Service; 7 8 import com.shux.inventory.biz.InventoryProductBiz; 9 import com.shux.inventory.entity.InventoryProduct; 10 import com.shux.inventory.mapper.InventoryProductMapper; 11 import com.shux.redis.biz.IRedisBiz; 12 13 /** 14 ********************************************** 15 * 描述 16 * Simba.Hua 17 * 2017年8月30日 18 ********************************************** 19 **/ 20 @Service("inventoryProductBiz") 21 public class InventoryProductBizImpl implements InventoryProductBiz { 22 private @Autowired IRedisBiz<InventoryProduct> redisBiz; 23 private @Resource InventoryProductMapper mapper; 24 @Override 25 public void updateInventoryProduct(InventoryProduct inventoryProduct) { 26 // TODO Auto-generated method stub 27 mapper.updateInventoryProduct(inventoryProduct); 28 } 29 30 @Override 31 public InventoryProduct loadInventoryProductByProductId(Integer productId) { 32 // TODO Auto-generated method stub 33 return mapper.loadInventoryProductByProductId(productId); 34 } 35 36 @Override 37 public void setInventoryProductCache(InventoryProduct inventoryProduct) { 38 redisBiz.set("inventoryProduct:"+inventoryProduct.getProductId(), inventoryProduct); 39 40 } 41 42 @Override 43 public void removeInventoryProductCache(Integer productId) { 44 redisBiz.delete("inventoryProduct:"+productId); 45 46 } 47 48 @Override 49 public InventoryProduct loadInventoryProductCache(Integer productId) { 50 // TODO Auto-generated method stub 51 return redisBiz.get("inventoryProduct:"+productId); 52 } 53 54 }
數據查詢請求controller
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 }