redis系列之数据库与缓存数据一致性解决方案
redis系列之数据库与缓存数据一致性解决方案
数据库与缓存读写模式策略
数据库与缓存双写情况下导致数据不一致问题
场景一
场景一解决方案
场景二
场景二解决方案
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。
(1)读请求时长阻塞
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时间内返回,该解决方案最大的风险在于可能数据更新很频繁,导致队列中挤压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,像遇到这种情况,一般要做好足够的压力测试,如果压力过大,需要根据实际情况添加机器。
(2)请求并发量过高
这里还是要做好压力测试,多模拟真实场景,并发量在最高的时候QPS多少,扛不住就要多加机器,还有就是做好读写比例是多少
(3)多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上
(4)热点商品的路由问题,导致请求的倾斜
某些商品的读请求特别高,全部打到了相同的机器的相同丢列里了,可能造成某台服务器压力过大,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是很大,但是确实有可能某些服务器的负载会高一些。
数据库与缓存数据一致性解决方案流程图
数据库与缓存数据一致性解决方案对应代码
商品库存实体
- package com.shux.inventory.entity;
- /**
- **********************************************
- * 描述:
- * Simba.Hua
- * 2017年8月30日
- **********************************************
- **/
- public class InventoryProduct {
- private Integer productId;
- private Long InventoryCnt;
- public Integer getProductId() {
- return productId;
- }
- public void setProductId(Integer productId) {
- this.productId = productId;
- }
- public Long getInventoryCnt() {
- return InventoryCnt;
- }
- public void setInventoryCnt(Long inventoryCnt) {
- InventoryCnt = inventoryCnt;
- }
- }
请求接口
- /**
- **********************************************
- * 描述:
- * Simba.Hua
- * 2017年8月27日
- **********************************************
- **/
- public interface Request {
- public void process();
- public Integer getProductId();
- public boolean isForceFefresh();
- }
数据更新请求
- package com.shux.inventory.request;
- import org.springframework.transaction.annotation.Transactional;
- import com.shux.inventory.biz.InventoryProductBiz;
- import com.shux.inventory.entity.InventoryProduct;
- /**
- **********************************************
- * 描述:更新库存信息
- * 1、先删除缓存中的数据
- * 2、更新数据库中的数据
- * Simba.Hua
- * 2017年8月30日
- **********************************************
- **/
- public class InventoryUpdateDBRequest implements Request{
- private InventoryProductBiz inventoryProductBiz;
- private InventoryProduct inventoryProduct;
- public InventoryUpdateDBRequest(InventoryProduct inventoryProduct,InventoryProductBiz inventoryProductBiz){
- this.inventoryProduct = inventoryProduct;
- this.inventoryProductBiz = inventoryProductBiz;
- }
- @Override
- @Transactional
- public void process() {
- inventoryProductBiz.removeInventoryProductCache(inventoryProduct.getProductId());
- inventoryProductBiz.updateInventoryProduct(inventoryProduct);
- }
- @Override
- public Integer getProductId() {
- // TODO Auto-generated method stub
- return inventoryProduct.getProductId();
- }
- @Override
- public boolean isForceFefresh() {
- // TODO Auto-generated method stub
- return false;
- }
- }
查询请求
- package com.shux.inventory.request;
- import com.shux.inventory.biz.InventoryProductBiz;
- import com.shux.inventory.entity.InventoryProduct;
- /**
- **********************************************
- * 描述:查询缓存数据
- * 1、从数据库中查询
- * 2、从数据库中查询后插入到缓存中
- * Simba.Hua
- * 2017年8月30日
- **********************************************
- **/
- public class InventoryQueryCacheRequest implements Request {
- private InventoryProductBiz inventoryProductBiz;
- private Integer productId;
- private boolean isForceFefresh;
- public InventoryQueryCacheRequest(Integer productId,InventoryProductBiz inventoryProductBiz,boolean isForceFefresh) {
- this.productId = productId;
- this.inventoryProductBiz = inventoryProductBiz;
- this.isForceFefresh = isForceFefresh;
- }
- @Override
- public void process() {
- InventoryProduct inventoryProduct = inventoryProductBiz.loadInventoryProductByProductId(productId);
- inventoryProductBiz.setInventoryProductCache(inventoryProduct);
- }
- @Override
- public Integer getProductId() {
- // TODO Auto-generated method stub
- return productId;
- }
- public boolean isForceFefresh() {
- return isForceFefresh;
- }
- public void setForceFefresh(boolean isForceFefresh) {
- this.isForceFefresh = isForceFefresh;
- }
- }
spring启动时初始化队列线程池
- package com.shux.inventory.thread;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import com.shux.inventory.request.Request;
- import com.shux.inventory.request.RequestQueue;
- import com.shux.utils.other.SysConfigUtil;
- /**
- **********************************************
- * 描述:请求处理线程池,初始化队列数及每个队列最多能处理的数量
- * Simba.Hua
- * 2017年8月27日
- **********************************************
- **/
- public class RequestProcessorThreadPool {
- private static final int blockingQueueNum = SysConfigUtil.get("request.blockingqueue.number")==null?10:Integer.valueOf(SysConfigUtil.get("request.blockingqueue.number").toString());
- private static final int queueDataNum = SysConfigUtil.get("request.everyqueue.data.length")==null?100:Integer.valueOf(SysConfigUtil.get("request.everyqueue.data.length").toString());
- private ExecutorService threadPool = Executors.newFixedThreadPool(blockingQueueNum);
- private RequestProcessorThreadPool(){
- for(int i=0;i<blockingQueueNum;i++){//初始化队列
- ArrayBlockingQueue<Request> queue = new ArrayBlockingQueue<Request>(queueDataNum);//每个队列中放100条数据
- RequestQueue.getInstance().addQueue(queue);
- threadPool.submit(new RequestProcessorThread(queue));//把每个queue交个线程去处理,线程会处理每个queue中的数据
- }
- }
- public static class Singleton{
- private static RequestProcessorThreadPool instance;
- static{
- instance = new RequestProcessorThreadPool();
- }
- public static RequestProcessorThreadPool getInstance(){
- return instance;
- }
- }
- public static RequestProcessorThreadPool getInstance(){
- return Singleton.getInstance();
- }
- /**
- * 初始化线程池
- */
- public static void init(){
- getInstance();
- }
- }
请求处理线程
- package com.shux.inventory.thread;
- import java.util.Map;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.Callable;
- import com.shux.inventory.request.InventoryUpdateDBRequest;
- import com.shux.inventory.request.Request;
- import com.shux.inventory.request.RequestQueue;
- /**
- **********************************************
- * 描述:请求处理线程
- * Simba.Hua
- * 2017年8月27日
- **********************************************
- **/
- public class RequestProcessorThread implements Callable<Boolean>{
- private ArrayBlockingQueue<Request> queue;
- public RequestProcessorThread(ArrayBlockingQueue<Request> queue){
- this.queue = queue;
- }
- @Override
- public Boolean call() throws Exception {
- Request request = queue.take();
- Map<Integer,Boolean> flagMap = RequestQueue.getInstance().getFlagMap();
- //不需要强制刷新的时候,查询请求去重处理
- if (!request.isForceFefresh()){
- if (request instanceof InventoryUpdateDBRequest) {//如果是更新请求,那就置为false
- flagMap.put(request.getProductId(), true);
- } else {
- Boolean flag = flagMap.get(request.getProductId());
- /**
- * 标志位为空,有三种情况
- * 1、没有过更新请求
- * 2、没有查询请求
- * 3、数据库中根本没有数据
- * 在最初情况,一旦库存了插入了数据,那就好会在缓存中也会放一份数据,
- * 但这种情况下有可能由于redis中内存满了,redis通过LRU算法把这个商品给清除了,导致缓存中没有数据
- * 所以当标志位为空的时候,需要从数据库重查询一次,并且把标志位置为false,以便后面的请求能够从缓存中取
- */
- if ( flag == null) {
- flagMap.put(request.getProductId(), false);
- }
- /**
- * 如果不为空,并且flag为true,说明之前有一次更新请求,说明缓存中没有数据了(更新缓存会先删除缓存),
- * 这个时候就要去刷新缓存,即从数据库中查询一次,并把标志位设置为false
- */
- if ( flag != null && flag) {
- flagMap.put(request.getProductId(), false);
- }
- /**
- * 这种情况说明之前有一个查询请求,并且把数据刷新到了缓存中,所以这时候就不用去刷新缓存了,直接返回就可以了
- */
- if (flag != null && !flag) {
- flagMap.put(request.getProductId(), false);
- return true;
- }
- }
- }
- request.process();
- return true;
- }
- }
请求队列
- package com.shux.inventory.request;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.ConcurrentHashMap;
- /**
- **********************************************
- * 描述:请求队列
- * Simba.Hua
- * 2017年8月27日
- **********************************************
- **/
- public class RequestQueue {
- private List<ArrayBlockingQueue<Request>> queues = new ArrayList<>();
- private Map<Integer,Boolean> flagMap = new ConcurrentHashMap<>();
- private RequestQueue(){
- }
- private static class Singleton{
- private static RequestQueue queue;
- static{
- queue = new RequestQueue();
- }
- public static RequestQueue getInstance() {
- return queue;
- }
- }
- public static RequestQueue getInstance(){
- return Singleton.getInstance();
- }
- public void addQueue(ArrayBlockingQueue<Request> queue) {
- queues.add(queue);
- }
- public int getQueueSize(){
- return queues.size();
- }
- public ArrayBlockingQueue<Request> getQueueByIndex(int index) {
- return queues.get(index);
- }
- public Map<Integer,Boolean> getFlagMap() {
- return this.flagMap;
- }
- }
spring 启动初始化线程池类
- package com.shux.inventory.listener;
- import org.springframework.context.ApplicationListener;
- import org.springframework.context.event.ContextRefreshedEvent;
- import com.shux.inventory.thread.RequestProcessorThreadPool;
- /**
- **********************************************
- * 描述:spring 启动初始化线程池类
- * Simba.Hua
- * 2017年8月27日
- **********************************************
- **/
- public class InitListener implements ApplicationListener<ContextRefreshedEvent>{
- @Override
- public void onApplicationEvent(ContextRefreshedEvent event) {
- // TODO Auto-generated method stub
- if(event.getApplicationContext().getParent() != null){
- return;
- }
- RequestProcessorThreadPool.init();
- }
- }
异步处理请求接口
- package com.shux.inventory.biz;
- import com.shux.inventory.request.Request;
- /**
- **********************************************
- * 描述:请求异步处理接口,用于路由队列并把请求加入到队列中
- * Simba.Hua
- * 2017年8月30日
- **********************************************
- **/
- public interface IRequestAsyncProcessBiz {
- void process(Request request);
- }
异步处理请求接口实现
- package com.shux.inventory.biz.impl;
- import java.util.concurrent.ArrayBlockingQueue;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.stereotype.Service;
- import com.shux.inventory.biz.IRequestAsyncProcessBiz;
- import com.shux.inventory.request.Request;
- import com.shux.inventory.request.RequestQueue;
- /**
- **********************************************
- * 描述:异步处理请求,用于路由队列并把请求加入到队列中
- * Simba.Hua
- * 2017年8月30日
- **********************************************
- **/
- @Service("requestAsyncProcessService")
- public class RequestAsyncProcessBizImpl implements IRequestAsyncProcessBiz {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Override
- public void process(Request request) {
- // 做请求的路由,根据productId路由到对应的队列
- ArrayBlockingQueue<Request> queue = getQueueByProductId(request.getProductId());
- try {
- queue.put(request);
- } catch (InterruptedException e) {
- logger.error("产品ID{}加入队列失败",request.getProductId(),e);
- }
- }
- private ArrayBlockingQueue<Request> getQueueByProductId(Integer productId) {
- RequestQueue requestQueue = RequestQueue.getInstance();
- String key = String.valueOf(productId);
- int hashcode;
- int hash = (key == null) ? 0 : (hashcode = key.hashCode())^(hashcode >>> 16);
- //对hashcode取摸
- int index = (requestQueue.getQueueSize()-1) & hash;
- return requestQueue.getQueueByIndex(index);
- }
- }
数据更新请求controller
- package com.shux.inventory.controller;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- import com.shux.inventory.biz.IRequestAsyncProcessBiz;
- import com.shux.inventory.biz.InventoryProductBiz;
- import com.shux.inventory.entity.InventoryProduct;
- import com.shux.inventory.request.InventoryUpdateDBRequest;
- import com.shux.inventory.request.Request;
- import com.shux.utils.other.Response;
- /**
- **********************************************
- * 描述:提交更新请求
- * Simba.Hua
- * 2017年9月1日
- **********************************************
- **/
- @Controller("/inventory")
- public class InventoryUpdateDBController {
- private @Autowired InventoryProductBiz inventoryProductBiz;
- private @Autowired IRequestAsyncProcessBiz requestAsyncProcessBiz;
- @RequestMapping("/updateDBInventoryProduct")
- @ResponseBody
- public Response updateDBInventoryProduct(InventoryProduct inventoryProduct){
- Request request = new InventoryUpdateDBRequest(inventoryProduct,inventoryProductBiz);
- requestAsyncProcessBiz.process(request);
- return new Response(Response.SUCCESS,"更新成功");
- }
- }
数据查询请求controller
- package com.shux.inventory.controller;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.RequestMapping;
- import com.shux.inventory.biz.IRequestAsyncProcessBiz;
- import com.shux.inventory.biz.InventoryProductBiz;
- import com.shux.inventory.entity.InventoryProduct;
- import com.shux.inventory.request.InventoryQueryCacheRequest;
- import com.shux.inventory.request.Request;
- /**
- **********************************************
- * 描述:提交查询请求
- * 1、先从缓存中取数据
- * 2、如果能从缓存中取到数据,则返回
- * 3、如果不能从缓存取到数据,则等待20毫秒,然后再次去数据,直到200毫秒,如果超过200毫秒还不能取到数据,则从数据库中取,并强制刷新缓存数据
- * Simba.Hua
- * 2017年9月1日
- **********************************************
- **/
- @Controller("/inventory")
- public class InventoryQueryCacheController {
- private @Autowired InventoryProductBiz inventoryProductBiz;
- private @Autowired IRequestAsyncProcessBiz requestAsyncProcessBiz;
- @RequestMapping("/queryInventoryProduct")
- public InventoryProduct queryInventoryProduct(Integer productId) {
- Request request = new InventoryQueryCacheRequest(productId,inventoryProductBiz,false);
- requestAsyncProcessBiz.process(request);//加入到队列中
- long startTime = System.currentTimeMillis();
- long allTime = 0L;
- long endTime = 0L;
- InventoryProduct inventoryProduct = null;
- while (true) {
- if (allTime > 200){//如果超过了200ms,那就直接退出,然后从数据库中查询
- break;
- }
- try {
- inventoryProduct = inventoryProductBiz.loadInventoryProductCache(productId);
- if (inventoryProduct != null) {
- return inventoryProduct;
- } else {
- Thread.sleep(20);//如果查询不到就等20毫秒
- }
- endTime = System.currentTimeMillis();
- allTime = endTime - startTime;
- } catch (Exception e) {
- }
- }
- /**
- * 代码执行到这来,只有以下三种情况
- * 1、缓存中本来有数据,由于redis内存满了,redis通过LRU算法清除了缓存,导致数据没有了
- * 2、由于之前数据库查询比较慢或者内存太小处理不过来队列中的数据,导致队列里挤压了很多的数据,所以一直没有从数据库中获取数据然后插入到缓存中
- * 3、数据库中根本没有这样的数据,这种情况叫数据穿透,一旦别人知道这个商品没有,如果一直执行查询,就会一直查询数据库,如果过多,那么有可能会导致数据库瘫痪
- */
- inventoryProduct = inventoryProductBiz.loadInventoryProductByProductId(productId);
- if (inventoryProduct != null) {
- Request forcRrequest = new InventoryQueryCacheRequest(productId,inventoryProductBiz,true);
- requestAsyncProcessBiz.process(forcRrequest);//这个时候需要强制刷新数据库,使缓存中有数据
- return inventoryProduct;
- }
- return null;
- }
- }
redis系列之数据库与缓存数据一致性解决方案的更多相关文章
- 用Redis作为Mysql数据库的缓存【转】
用Redis作Mysql数据库缓存,必须解决2个问题.首先,应该确定用何种数据结构存储来自Mysql的数据:在确定数据结构之后,还要考虑用什么标识作为该数据结构的键. 直观上看,Mysql中的数据都是 ...
- redis系列之------数据库
前言 当我们在Redis数据库中set一个KV的时候,这个KV保存在哪里?如果我们get的时候,又从哪里get出来.时间复杂度,空间复杂的等等,怎么优化等等一系列问题. 服务器中的数据库 Redis服 ...
- redis(三)--用Redis作为Mysql数据库的缓存
把MySQL结果集缓存到Redis的字符串或哈希结构中以后,我们面临一个新的问题,即如何为这些字符串或哈希命名,也就是如何确定它们的键.因为这些数据结构所对应的行都属于某个结果集,假如可以找到一种唯一 ...
- 用Redis作为Mysql数据库的缓存
看到一篇不错的博文,记录下: http://blog.csdn.net/qtyl1988/article/details/39553339 http://blog.csdn.net/qtyl1988/ ...
- 数据库 | Redis 缓存雪崩解决方案
Redis 雪崩 缓存层承载着大量的请求,有效保护了存储层.但是如果由于缓存大量失效或者缓存整体不能提供服务,导致大量的请求到达存储层,会使存储层负载增加,这就是缓存雪崩的场景. 解决缓存雪崩,可以从 ...
- Redis缓存穿透、缓存雪崩、redis并发问题 并发竞争key的解决方案 (阿里)
阿里的人问我 缓存雪崩(大量数据在同一时间过期了)了如何处理,缓存击穿了如何处理,回答的很烂,做了总结: 把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数 ...
- Redis总结(五)缓存雪崩和缓存穿透等问题 Web API系列(三)统一异常处理 C#总结(一)AutoResetEvent的使用介绍(用AutoResetEvent实现同步) C#总结(二)事件Event 介绍总结 C#总结(三)DataGridView增加全选列 Web API系列(二)接口安全和参数校验 RabbitMQ学习系列(六): RabbitMQ 高可用集群
Redis总结(五)缓存雪崩和缓存穿透等问题 前面讲过一些redis 缓存的使用和数据持久化.感兴趣的朋友可以看看之前的文章,http://www.cnblogs.com/zhangweizhon ...
- Redis与DB的数据一致性解决方案(史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...
- Redis缓存穿透、击穿、雪崩,数据库与缓存一致性
Redis作为高性能非关系型(NoSQL)的键值对数据库,受到了广大用户的喜爱和使用,大家在项目中都用到了Redis来做数据缓存,但有些问题我们在使用中不得不考虑,其中典型的问题就是:缓存穿透.缓存雪 ...
随机推荐
- swift--CATransform3D的简单介绍
今天来了解下CATransform3D的一些基本的知识.CATransform3D是一个用于处理3D形变的类,其可以改变控件的平移.缩放.旋转.斜交等,其坐标系统采用的是三维坐标系,即向右为x轴正方向 ...
- 利用Visio绘制数据流图与组织结构图
绘制数据流图: 利用Visio 2007来绘制网上书店系统的数据流图.利用Visio 2007创建Gane- Sarson数据流图,可以选择“软件和数据库”模板,然后再选择“数据流模型图”,创建之后可 ...
- 【RF库XML测试】Get Elements
Name:Get ElementsSource:XML <test library>Arguments:[ source | xpath ]Returns a list of elemen ...
- goto的用法
using UnityEngine; using System.Collections; public class goto1 : MonoBehaviour { public bool can = ...
- Ubuntu输入法切换问题
不知道改了个什么东西,Ubuntu 15.04 中Ctrl+Space不能切换输入法了,因此不能输入英文,shell就更是没法工作,在设置里面找了好久,“文本输入”/“语言支持”/“键盘”里面都没找到 ...
- Windows 系统提示“内存不足”的原因及解决方法
Windows 系统提示“内存不足”的原因及解决方法 windows XP vista 及windows 7系统的电脑有时候会出现系统提示“内存不足”,这是由多方面原因造成的.本文具体分析下 ...
- 浅析TCP字节流与UDP数据报的区别
转自http://www.linuxidc.com/Linux/2014-11/109545.htm “TCP是一种流模式的协议,UDP是一种数据报模式的协议”,这句话相信大家对这句话已经耳熟能详~但 ...
- cookie设置在特定时间点过期的方法
假设需求为:在当天晚上0:00过期. 方法: 得到当天晚上0:00这个时间点的一个时间. function getNextDate(){ var d = new Date(), ...
- Window 命令行神器:cmder
http://cmder.net/ https://github.com/cmderdev/cmder/releases/ 官网下载地址 http://www.360doc.com/content ...
- OpenStack网络详解
本博客已经添加"打赏"功能,"打赏"位置位于右边栏红色框中,感谢您赞助的咖啡. Openstack需要对网络有一些了解才能进入openstack的世界,很多都是 ...