并发场景中大部分处理的是先更新DB,再(删缓、更新)缓存的处理方式,但是在实际场景中有可能DB更新成功了,但是缓存设置失败了,就造成了缓存与DB数据不一致的问题,下面就以实际情况说下怎么解决此类问题。

  名词 Cache:本文内指redis,ReadRequest:请求从Cache、Db中拿去数据,WriteRequest:数据写入DB并删除缓存

  若要保证数据库与缓存一直,我们需要采用先删缓存,在更新DB的情况,这时候有的同学可能会问,如果缓存删除成功了,而DB更新失败了怎么办,其实仔细考虑一下,DB虽然失败了,那真正是不会产生数据影响的,而当下次一次请求进来的时候,我们重新把DB中未更新的数据重新塞入缓存,从结果上来看是没有影响的。我们把请求分为ReadRequest 、WriteRequest,大部分同学都知道我们在使用Cache时 首先都会去Cache内查一下,如果Cache中没有拿到数据我们在从数据库中去获取数据,这个时候在高并发的场景的踩过坑的同学都知道恰巧在这时候有更新请求把缓存删除了,这时候大量请求进来,Cache内没有此项数据,请求就会直接落在DB上,就很容易造成缓存雪崩,数据库很可能瞬时就挂掉了,所以处理方案就是我们需要对查询写入的缓存进行排队处理,而正确从cache内获取的姿势:

  1、每次查询数据的时候我们吧请求数据放入队列,由队列消费者去检查一下cache是否存在,不存在则进行插入,存在就跳过

  2、当前readRequest就自循环,我们不断尝试从cache内去获取数据,拿到数据或超时当前线程立即退出

  3、如果拿到数据了就返回结果,没有拿到数据我们就从DB去查

  而WriteRequest 的处理相对就简单多了我们直接删除缓存后,更新DB即可,下面上代码说明:

  消息队列这里我们基于jdk并发包内的BlockingQueue进行实现,使用MQ(Rabbit,Kafka等)的话思想差不多,只是需要交互一次mq的服务端。首先项目启动时我们在程序后台开辟监听线程,从数据共享缓冲区(ArrayBlockingQueue)内监听消息

public class BlockQueueThreadPool {

    /**
* 核心线程数
*/
private Integer corePoolSize = ;
/**
* 线程池最大线程数
*/
private Integer maximumPoolSize = ; /**
* 线程最大存活时间
*/
private Long keepAliveTime = 60L; private ExecutorService threadPool = new ThreadPoolExecutor(this.corePoolSize, this.maximumPoolSize,
this.keepAliveTime, TimeUnit.SECONDS,
new ArrayBlockingQueue(this.corePoolSize)); public BlockQueueThreadPool() {
RequestQueue requestQueue = RequestQueue.getInstance();
BlockingQueue<RequestAction> queue = new ArrayBlockingQueue<>(this.corePoolSize);
requestQueue.add(queue);
this.threadPool.submit(new JobThread(queue));
}
}

   PS:ArrayBlockingQueue中很好的利用了Condition中的等待和通知功能,这里我们就能实现对共享通道队列的事件监听了。

public class JobThread implements Callable<Boolean> {
private BlockingQueue<RequestAction> queue; public JobThread(BlockingQueue<RequestAction> queue) {
this.queue = queue;
} @Override
public Boolean call() throws Exception {
try {
while (true) {
// ArrayBlockingQueue take方法 获取队列排在首位的对象,如果队列为空或者队列满了,则会被阻塞住
RequestAction request = this.queue.take();
RequestQueue requestQueue = RequestQueue.getInstance();
Map<String, Boolean> tagMap = requestQueue.getTagMap();
if (request instanceof ReadRequest) {
Boolean tag = tagMap.get(request.getIdentity());
if (null == tag) {
tagMap.put(request.getIdentity(), Boolean.FALSE);
}
if (tag != null && tag) {
tagMap.put(request.getIdentity(), Boolean.FALSE);
}
if (tag != null && !tag) {
return Boolean.TRUE;
} } else if (request instanceof WriteRequest) {
// 如果是更新数据库的操作
tagMap.put(request.getIdentity(), Boolean.TRUE);
} // 执行请求处理
log.info("缓存队列执行+++++++++++++++++,{}", request.getIdentity());
request.process();
}
} catch (Exception e) {
e.printStackTrace();
}
return Boolean.TRUE;
}
}

  接下来就要定义我们的WriteRequest、ReadRequest了

@Slf4j
public class ReadRequest<TResult> extends BaseRequest { public ReadRequest(String cacheKey, GetDataSourceInterface action) {
super(cacheKey, action);
} @Override
public void process() {
TResult result = (TResult) action.exec();
if (Objects.isNull(result)) {
//防止缓存击穿
redis.set(cacheKey, "", );
} else {
redis.set(cacheKey, result, );
}
}
}
public class WriteRequest<TResult> extends BaseRequest {

    public WriteRequest(String cacheKey, GetDataSourceInterface action) {
super(cacheKey, action);
} @Override
public void process() {
redis.del(cacheKey);
action.exec();
}
}

  这里我们需要坐下判断,在数据库内查询数据为空后把“”写入了缓存,这样子是避免有人恶意请求不存在的数据时造成缓存击穿。接下来就是我们针对各项业务场景中需要获取与更新缓存的路由端了

@UtilityClass
public class RouteUtils {
public static void route(RequestAction requestAction) {
try {
BlockingQueue<RequestAction> queue = RequestQueue.getInstance().getQueue();
queue.put(requestAction); } catch (Exception e) {
e.printStackTrace(); }
}
}
public class RequestQueue {

    private RequestQueue() {
} private List<BlockingQueue<RequestAction>> queues = new ArrayList<>(); private Map<String, Boolean> tagMap = new ConcurrentHashMap<>(); private static class Singleton {
private static RequestQueue queue; static {
queue = new RequestQueue();
} private static RequestQueue getInstance() {
return queue;
}
} public static RequestQueue getInstance() {
return Singleton.getInstance();
} public void add(BlockingQueue<RequestAction> queue) {
this.queues.add(queue);
} public BlockingQueue<RequestAction> getQueue(int index) {
return this.queues.get(index);
} public int size() {
return this.queues.size();
} public Map<String, Boolean> getTagMap() {
return this.tagMap;
}
}

  这里有一个小的知识点,很多时候我们在保证线程安全的时候多数会使用DSL双锁模型,但是我始终觉得这类代码不够美观,所以我们可以利用JVM的类加载原则,使用静态类包裹初始化类,这样子也一定能保证单例模型,并且代码也更美观了。接下来就可以看下Service的代码

@Service
public class StudentService { public Student getStudent(String name) {
ReadRequest<Student> readRequest = new ReadRequest<>(name, () -> Student.builder().name(name).age().build());
return CacheProcessor.builder().build().getData(readRequest);
} public void update(Student student) {
WriteRequest<Student> writeRequest = new WriteRequest<>(student.getName(), () -> student);
CacheProcessor.builder().build().setData(writeRequest);
}
}

Service内直接调用了Cachce的处理者,我们通过处理者来获取缓存与更新缓存

@Builder
public class CacheProcessor {
public <TResult> TResult getData(ReadRequest readRequest) {
try {
RouteUtils.route(readRequest);
long startTime = System.currentTimeMillis();
long waitTime = 0L;
while (true) {
if (waitTime > ) {
break;
}
TResult result = (TResult) readRequest.redis.get(readRequest.getIdentity());
if (!Objects.isNull(result)) {
return result;
} else {
Thread.sleep();
waitTime = System.currentTimeMillis() - startTime;
}
}
return (TResult) readRequest.get();
} catch (Exception e) {
return null;
}
} public void setData(WriteRequest writeRequest){
RouteUtils.route(writeRequest);
}
}

  这里我们就先把请求数据发送到数据共享渠道,消费者端与当前的ReadRequest线程同步执行,拿到数据后ReadRequest就立马退出,超时后我们就从数据库中获取数据。这里面我使用了java8 @FunctionalInterface 标记接口,对各个业务中需要用到缓存的地方统一进行封装方便调用,以上的代码就已经基本说明并发中Db和Cache双休一致性的解决思路,聪明的小伙伴肯定能看出其实还有很多优化的地方,比如说我们栗子中是单线程吞吐量不高,采用多线程与多消费者端的时候我们还需要保证商品的更新和读取请求需要落在同一个消费者端等等问题。或者在使用外部MQ时,我们除了要考虑以上同一商品的读写保证落在一个消费节点上,还需要考虑队列内有插入缓存请求的时候需要跳过的处理等等,更多情况还需要根据实际情况大家自己去发现咯

参考:中华石杉的教程

  

并发中如何保证缓存DB双写一致性(JAVA栗子)的更多相关文章

  1. 第三节:Redis缓存雪崩、击穿、穿透、双写一致性、并发竞争、热点key重建优化、BigKey的优化 等解决方案

    一. 缓存雪崩 1. 含义 同一时刻,大量的缓存同时过期失效. 2. 产生原因和后果 (1). 原因:由于开发人员经验不足或失误,大量热点缓存设置了统一的过期时间. (2). 产生后果:恰逢秒杀高峰, ...

  2. 【原创】分布式之数据库和缓存双写一致性方案解析(三) 前端面试送命题(二)-callback,promise,generator,async-await JS的进阶技巧 前端面试送命题(一)-JS三座大山 Nodejs的运行原理-科普篇 优化设计提高sql类数据库的性能 简单理解token机制

    [原创]分布式之数据库和缓存双写一致性方案解析(三)   正文 博主本来觉得,<分布式之数据库和缓存双写一致性方案解析>,一文已经十分清晰.然而这一两天,有人在微信上私聊我,觉得应该要采用 ...

  3. Redis双写一致性与缓存更新策略

    一.双写一致性 双写一致性,也就是说 Redis 和 mysql 数据同步 双写一致性数据同步的方案有: 1.先更新数据库,再更新缓存 这个方案一般不用: 因为当有两个请求AB先后更新数据库后,A应该 ...

  4. PHP经典面试题:如何保证缓存与数据库的双写一致性?

    只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? 面试题剖析 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说 ...

  5. PHP中高级面试题 一个高频面试题:怎么保证缓存与数据库的双写一致性?

    分布式缓存是现在很多分布式应用中必不可少的组件,但是用到了分布式缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? Cache Aside ...

  6. K:缓存数据库双写数据一致性方案

    对于缓存和数据库双写,其存在着数据一致性的问题.对于数据一致性要求较高的业务场景,我们通常会选择使用分布式事务(2pc.paxos等)来保证缓存与数据库之间的数据强一致性,但分布式事务的复杂性与对资源 ...

  7. 《Redis Mysql 双写一致性问题》

    一:序 - 最近在对数据做缓存时候,会涉及到如何保证 数据库/Redis 一致性问题. - 刚好今天来总结下 一致性问题 产生的问题,和可能存在的解决方案. 二:(更新策略)-  先更新数据库,后更新 ...

  8. Redis Mysql 双写一致性问题

    一:序 - 最近在对数据做缓存时候,会涉及到如何保证 数据库/Redis 一致性问题. - 刚好今天来总结下 一致性问题 产生的问题,和可能存在的解决方案. 二:(更新策略)-  先更新数据库,后更新 ...

  9. Redis面试篇 -- 如何保证缓存与数据库的双写一致性?

    如果不是严格要求“缓存和数据库”必须保证一致性的话,最好不要做这个方案:即 读请求和写请求串行化,串到一个内存队列里面去.串行化可以保证一定不会出现不一致的情况,但会导致系统吞吐量大幅度降低. 解决这 ...

随机推荐

  1. html实现打印预览效果

    前面说到利用lodop插件进行打印设置,那个应用于打印快递面单,或者跟快递面单相似场景的情况. 今天的利用html快速打印出A4纸大小的场景,例如:合同.静态文本等. 效果如下: 方式一 1.设置di ...

  2. 【集群监控】JMX exporter+Prometheus+Grafana监控Hadoop集群

    下载jmx_exporter的jar包 https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.3.1/ ...

  3. Drill 学习笔记之 入门体验

    简介: Apache Drill是一个低延迟的分布式海量数据(涵盖结构化.半结构化以及嵌套数据)交互式查询引擎.分布式.无模式(schema-free) 是Google Dremel的开源实现,本质是 ...

  4. GAN算法笔记

    本篇文章为Goodfellow提出的GAN算法的开山之作"Generative Adversarial Nets"的学习笔记,若有错误,欢迎留言或私信指正. 1. Introduc ...

  5. 搭建docker+swoole+php7 的环境

    最近在学习swoole php扩展,苦恼于其运行环境不能在win系统下运行, 但开发代码一直在win系统上,很无奈,,,, 所以就用docker来代替,舒服~ 有很多相关docker的swoole镜像 ...

  6. 如何去除CFormView的Scrollbar

    第一种方法: 重载 OnSize(UINT nType, int cx, int cy) 在CFormView::OnSize(nType, cx, cy)下面添加一句 ShowScrollBar(S ...

  7. Java中NIO及基础实现

    NIO:同步非阻塞IO 来源:BIO是同步阻塞IO操作,当线程在处理任务时,另一方会阻塞着等待该线程的执行完毕,为了提高效率,,JDK1.4后,引入NIO来提升数据的通讯性能 NIO中采用Reacto ...

  8. Linux之shell基础

    Shell基础 一.shell概述 1) shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动.挂起.停止甚至是编写一些程序 ...

  9. Powershell基础之脚本执行

    Bat 这就是我们常用的Bat脚本,全名为批处理文件,脚本中就是我们在CMD中使用到的命令,这里提一个小问题:CMD的命令行执行命令的优先级是.bat > .exe,那么假如我放一个cmd.ba ...

  10. pycharm2018.2汉化 解决设置打不开问题

    首先检查下是不是装了中文汉化包resources_cn.jar 如果有的话,解决办法:1.更换一个汉化包或者将原来的resources_en.jar也放进lib目录下 2.将汉化包都删除,只留下原版的 ...