并发场景中大部分处理的是先更新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. 遇到不支持的 Oracle 数据类型 USERDEFINED

    以前都是sql查询mdb空间数据没有什么问题,今天在用sql方式查询Oracle中的空间数据时候,出现错误.它不支持geometry.空间数据都带有shape属性.只要不查询shape字段就没问题.但 ...

  2. [UWP] 解决FlipView图片放大的诡异bug

    想要实现图片的放大缩小可以通过在Image外面套一个ScrollViewer,然后设置ScrollViewer的ZoomMode="Enabled" <FlipView It ...

  3. 报错fatal: refusing to merge unrelated histories

    提交到远程仓库的时候报错如下 是因为远程仓库有东西更新,但本地仓库没有更新造成提交失败 需要先把远程仓库给拉取下来,执行命令git pull origin master,又报错了如下 是因为两个仓库提 ...

  4. mvvm的初步思想

    1.Object.defineProperty(obj,key,desc); 用法:1.给对象新增属性和特性 2.修改对象属性值和特性 desc(属性特性): 1.enumerable:boolean ...

  5. 【TencentOS tiny】深度源码分析(8)——软件定时器

    软件定时器的基本概念 TencentOS tiny 的软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,本质上软件定时器的使用相当 ...

  6. 技术不错的Java程序员,为何面试却“屡战屡败”

    为何很多有不少编程经验,技术能力不错的程序员,去心仪公司面试时却总是失败?至于失败的原因,可能很多人都没意识到过. 01想要通关面试,千万别让数据结构拖了后腿 很多公司,比如 BAT.Google.F ...

  7. 【原创】(八)Linux内存管理 - zoned page frame allocator - 3

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  8. js仓库。。。

    <script type="text/javascript" src="//ra.revolvermaps.com/0/0/8.js?i=0ln1fndtptz&a ...

  9. python 中的一点新知识

    逻辑行与物理行 所谓物理行(Physical Line)是你在编写程序时 你所看到 的内容.所谓逻辑行(Logical Line)是 Python 所看到 的单个语句.Python 会假定每一 物理行 ...

  10. 最简单的ArcGIS Engine应用程序(下)

    在中篇我们讲到使用OpenFileDialog控件可以添加shp文件.(最简单的ArcGIS Engine应用程序(中)) 添加lyr文件的操作也是大同小异的. using System; using ...