hi,你好!欢迎访问本站!登录
本站由网站地图腾讯云宝塔系统阿里云强势驱动
当前位置:首页 - 教程 - 杂谈 - 正文 君子好学,自强不息!

并发中怎样保证缓存DB双写一致性(JAVA栗子)

2019-11-18杂谈搜奇网42°c
A+ A-

  并发场景中大部份处置惩罚的是先更新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 = 10; /** * 线程池最大线程数 */
    private Integer maximumPoolSize = 20; /** * 线程最大存活时候 */
    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, "", 10000); } else { redis.set(cacheKey, result, 10000); } } }
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(0); 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<>(1); 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(3).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 > 3000) { break; } TResult result = (TResult) readRequest.redis.get(readRequest.getIdentity()); if (!Objects.isNull(result)) { return result; } else { Thread.sleep(20); 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时,我们除了要斟酌以上统一商品的读写保证落在一个消耗节点上,还须要斟酌行列内有插进去缓存要求的时刻须要跳过的处置惩罚等等,更多状况还须要根据现实状况人人本身去发明咯

 

参考:中华石杉的教程

  

 

  选择打赏方式
微信赞助

打赏

QQ钱包

打赏

支付宝赞助

打赏

  移步手机端
并发中怎样保证缓存DB双写一致性(JAVA栗子)

1、打开你手机的二维码扫描APP
2、扫描左则的二维码
3、点击扫描获得的网址
4、可以在手机端阅读此文章
未定义标签

本文来源:搜奇网

本文地址:https://www.sou7.cn/282310.html

关注我们:微信搜索“搜奇网”添加我为好友

版权声明: 本文仅代表作者个人观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。请记住本站网址https://www.sou7.cn/搜奇网。

发表评论

选填

必填

必填

选填

请拖动滑块解锁
>>