并发中如何保证缓存DB双写一致性(JAVA栗子)
并发场景中大部分处理的是先更新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栗子)的更多相关文章
- 第三节:Redis缓存雪崩、击穿、穿透、双写一致性、并发竞争、热点key重建优化、BigKey的优化 等解决方案
一. 缓存雪崩 1. 含义 同一时刻,大量的缓存同时过期失效. 2. 产生原因和后果 (1). 原因:由于开发人员经验不足或失误,大量热点缓存设置了统一的过期时间. (2). 产生后果:恰逢秒杀高峰, ...
- 【原创】分布式之数据库和缓存双写一致性方案解析(三) 前端面试送命题(二)-callback,promise,generator,async-await JS的进阶技巧 前端面试送命题(一)-JS三座大山 Nodejs的运行原理-科普篇 优化设计提高sql类数据库的性能 简单理解token机制
[原创]分布式之数据库和缓存双写一致性方案解析(三) 正文 博主本来觉得,<分布式之数据库和缓存双写一致性方案解析>,一文已经十分清晰.然而这一两天,有人在微信上私聊我,觉得应该要采用 ...
- Redis双写一致性与缓存更新策略
一.双写一致性 双写一致性,也就是说 Redis 和 mysql 数据同步 双写一致性数据同步的方案有: 1.先更新数据库,再更新缓存 这个方案一般不用: 因为当有两个请求AB先后更新数据库后,A应该 ...
- PHP经典面试题:如何保证缓存与数据库的双写一致性?
只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? 面试题剖析 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说 ...
- PHP中高级面试题 一个高频面试题:怎么保证缓存与数据库的双写一致性?
分布式缓存是现在很多分布式应用中必不可少的组件,但是用到了分布式缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? Cache Aside ...
- K:缓存数据库双写数据一致性方案
对于缓存和数据库双写,其存在着数据一致性的问题.对于数据一致性要求较高的业务场景,我们通常会选择使用分布式事务(2pc.paxos等)来保证缓存与数据库之间的数据强一致性,但分布式事务的复杂性与对资源 ...
- 《Redis Mysql 双写一致性问题》
一:序 - 最近在对数据做缓存时候,会涉及到如何保证 数据库/Redis 一致性问题. - 刚好今天来总结下 一致性问题 产生的问题,和可能存在的解决方案. 二:(更新策略)- 先更新数据库,后更新 ...
- Redis Mysql 双写一致性问题
一:序 - 最近在对数据做缓存时候,会涉及到如何保证 数据库/Redis 一致性问题. - 刚好今天来总结下 一致性问题 产生的问题,和可能存在的解决方案. 二:(更新策略)- 先更新数据库,后更新 ...
- Redis面试篇 -- 如何保证缓存与数据库的双写一致性?
如果不是严格要求“缓存和数据库”必须保证一致性的话,最好不要做这个方案:即 读请求和写请求串行化,串到一个内存队列里面去.串行化可以保证一定不会出现不一致的情况,但会导致系统吞吐量大幅度降低. 解决这 ...
随机推荐
- 小鸟初学Shell编程(七)变量引用及作用范围
变量引用 那么定义好变量,如何打印变量的值呢?举例下变量引用的方式. ${变量名}称作为对变量的引用 echo ${变量名}查看变量的值 ${变量名}在部分情况下可以省略成 $变量名 [root@li ...
- 3D漫游的分类 3D Navigation Taxonomy
在2001年CHI发表的论文中1,Tan等人提出了一种对3D漫游的分类方法. 当时关于3D漫游(3D Navigation)的研究主要分为两种:一种是发掘有关漫游的认知原则,一种是开发一些具体的漫游技 ...
- 前端深入之css篇|你真的了解“权重”吗?
写在前面 权重这个概念,相信对许多进行过前端开发的小伙伴来说肯定并不陌生,有时候一个样式添加不上,我们就会一个 !important 怼上去,一切就好像迎刃而解了.但还有的时候,!important也 ...
- python爬虫—— 抓取今日头条的街拍的妹子图
AJAX 是一种用于创建快速动态网页的技术. 通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新. 近期在学习获取j ...
- 你应该知道的简单易用的CSS技巧
作为前端,在工作中难免会遇到关于排版的问题,以下是我整理的一些关于CSS的技巧,希望对你能有帮助. 1.每个单词的首字母大写 一般我们会用JS实现,其实CSS就可以实现. JS代码: var str ...
- Chrome 浏览器默认样式覆盖自己 CSS 样式的解决
检查 HTML 源代码,DOCTYPE 的声明是否写正确. HTML5 的 DOCTYPE 声明规范: <!DOCTYPE html> 参考链接: css - User agent sty ...
- Github | 吴恩达新书《Machine Learning Yearning》完整中文版开源
最近开源了周志华老师的西瓜书<机器学习>纯手推笔记: 博士笔记 | 周志华<机器学习>手推笔记第一章思维导图 [博士笔记 | 周志华<机器学习>手推笔记第二章&qu ...
- python编程基础之七
运算关系:也就是常说比较运算,返回值只有True, False == 判断是否相等 != 判断是否不相等 > ,< ,>= , <= 判断是否大于,小于,大于等于,小于 ...
- Java 操作Word表格——创建嵌套表格、添加/复制表格行或列、设置表格是否禁止跨页断行
本文将对如何在Java程序中操作Word表格作进一步介绍.操作要点包括 如何在Word中创建嵌套表格. 对已有表格添加行或者列 复制已有表格中的指定行或者列 对跨页的表格可设置是否禁止跨页断行 创建表 ...
- nextjs:如何将静态资源发布到 CDN
nextjs 是基于 react 的服务端同构指出框架,在使用的过程中也多多少少遇到过几个问题,其中最大的问题就是静态资源的发布了. 1. 如何基于文件内容进行 hash 命名 Next.js use ...