游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

Redis又崩溃了?一次性搞定Redis实践中的常见问题!

  无论是在开发过程中还是在准备跑路的面试过程中,有关redis相关的,难免会涉及到四个特殊场景:缓存穿透、缓存雪崩、缓存击穿以及数据一致性。
  如果在开发中不注意这些场景的话,在高并发场景下有可能会导致系统崩溃,数据错乱等情况。现在,结合实际的业务场景来复现并解决这些问题。
  相关技术:springboot2。2。2mybatisplus3。1redis5。0hutool5。8缓存穿透
  缓存穿透是指查询缓存和数据库中都不存在的数据,导致所有的查询压力全部给到了数据库。
  比如查询一篇文章信息并对其进行缓存,一般的逻辑是先查询缓存中是否存在该文章,如果存在则直接返回,否则再查询数据库并将查询结果进行缓存。Slf4jServicepublicclassDocumentInfoServiceImplextendsServiceImplDocumentInfoMapper,DocumentInfoimplementsDocumentInfoService{ResourceprivateStringRedisTemplatestringRedisTemplate;OverridepublicDocumentInfogetDocumentDetail(intdocId){StringredisKeydoc::info::docId;StringobjstringRedisTemplate。opsForValue()。get(redisKey);DocumentInfodocumentInfonull;if(StrUtil。isNotEmpty(obj)){缓存命中log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{log。info(selectfromdb);documentInfothis。lambdaQuery()。eq(DocumentInfo::getId,docId)。one();if(ObjectUtil。isNotNull(documentInfo)){缓存结果stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5L,TimeUnit。SECONDS);}}returndocumentInfo;}}GetMapping(docqueryById)publicResultDocumentInfoqueryById(RequestParam(namedocId)IntegerdocId){returnResult。success(documentInfoService。getDocumentDetail(docId));}
  如果项目的并发量不大,这样写的话几乎没啥问题。如果项目的并发量很大,那么这就存在一个隐藏问题,如果在访问了一个不存在的文章(这个文章已经被分享出去,但是在后台可能是被删除或者下线状态),那么就会导致所有的请求全部需要到数据库中进行查询,从而给数据库造成压力,甚至造成宕机。
  比如
  http:127。0。0。1:8081docqueryById?docId不存在的id2023010510:18:57。954INFO19692〔nio8081exec8〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:58。121INFO19692〔nio8081exec5〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:58。350INFO19692〔io8081exec10〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:58。519INFO19692〔nio8081exec3〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:58。661INFO19692〔nio8081exec6〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:58。859INFO19692〔nio8081exec4〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:59。012INFO19692〔nio8081exec9〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010510:18:59。154INFO19692〔nio8081exec7〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb解决方案一:缓存空对象
  针对缓存穿透问题缓存空对象可以有效避免所产生的影响,当查询一条不存在的数据时,在缓存中存储一个空对象并设置一个过期时间(设置过期时间是为了避免出现数据库中存在了数据但是缓存中仍然是空数据现象),这样可以避免所有请求全部查询数据库的情况。查询对象不存在if(StrUtil。equals(obj,)){log。info(selectfromcache,datanotavailable);returnnull;}if(StrUtil。isNotEmpty(obj)){log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{log。info(selectfromdb);documentInfothis。lambdaQuery()。eq(DocumentInfo::getId,docId)。one();如果数据不存在,则缓存一个空对象并设置过期时间stringRedisTemplate。opsForValue()。set(redisKey,ObjectUtil。isNotNull(documentInfo)?JSONUtil。toJsonStr(documentInfo):,5L,TimeUnit。SECONDS);if(ObjectUtil。isNotNull(documentInfo)){stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5L,TimeUnit。SECONDS);}}2023010513:15:01。057INFO16600〔nio8081exec3〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromdb2023010513:15:01。214INFO16600〔nio8081exec4〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromcache,datanotavailable2023010513:15:01。384INFO16600〔nio8081exec5〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromcache,datanotavailable2023010513:15:01。540INFO16600〔nio8081exec6〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromcache,datanotavailable2023010513:15:01。720INFO16600〔nio8081exec7〕c。g。r。s。impl。DocumentInfoServiceImpl:selectfromcache,datanotavailable解决方案二:布隆过滤器
  缓存空对象的缺点在于无论数据存不存在都需要查询一次数据库,并且redis中存储了大量的空数据,这个时候可以采用布隆过滤器来解决。布隆过滤器可以简单的理解为由一个很长的二进制数组结合n个hash算法计算出n个数组下标,将这些数据下标置为1。
  在查找数据时,再次通过n个hash算法计算出数组下标,如果这些下标的值为1,表示该值可能存在(存在hash冲突的原因),如果为0,则表示该值一定不存在。布隆过滤器添加元素伪代码BitArr〔〕bitnewBitArr〔10000〕;新建一个二进制数组ListStringinsertDataArrays。asList(A,B,C);待添加元素for(StringinsertDatum:insertData){for(inti1;i3;i){使用3中hash算法计算出3个数组下标intbitIdxhashi(insertDatum);hash1(insertDatum),hash2(insertDatum),hash3(insertDatum)bit〔bitIdx〕1;将下标元素置为1}}
  布隆过滤器查找元素伪代码BitArr〔〕bitnewBitArr〔10000〕;for(inti1;i3;i){intbitIdxhashi(E);计算E的数组下标if(bit〔bitIdx〕0){如果对应的元素为0,则一定不存在returnfalse;}}returntrue;
  布隆过滤器的实现
  在使用布隆过滤器时有两个核心参数,分别是预估的数据量size以及期望的误判率fpp,这两个参数我们可以根据自己的业务场景和数据量进行自主设置。在实现布隆过滤器时,有两个核心问题,分别是hash函数的选取个数n以及确定bit数组的大小len。根据预估数据量size和误判率fpp,可以计算出bit数组的大小len。
  image。png根据预估数据量size和bit数组的长度大小len,可以计算出所需要的hash函数个数n。
  单机版布隆过滤器
  目前单机版的布隆过滤器实现方式有很多,比如Guava提供的BloomFilter,Hutool工具包中提供的BitMapBloomFilter等。以Guava为例,需要引入对应的依赖包,在BloomFilter类中提供了create方法来进行布隆过滤器的创建。dependencygroupIdcom。google。guavagroupIdguavaartifactIdversion21。0versiondependencypublicstaticBloomFilterIntegerlocalBloomFilterBloomFilter。create(Funnels。integerFunnel(),10000L,0。01);
  创建完成后,将需要筛选的数据同步到过滤器中。单机版布隆过滤器数据初始化PostConstructpublicvoidinitDocumentDataLocal(){ListDocumentInfodocumentInfosdocumentInfoService。lambdaQuery()。select(DocumentInfo::getId)。list();if(CollUtil。isNotEmpty(documentInfos)){documentInfos。stream()。map(DocumentInfo::getId)。forEach(e{BloomFilterUtil。localBloomFilter。put(e);});}}
  在业务代码中,可以直接调用BloomFilter提供的mightContain方法,判断目标docId是否可能存在于过滤器中,如果可能存在,那么继续向下执行业务逻辑,否则直接中断执行。OverridepublicDocumentInfogetDocumentDetail(intdocId){布隆过滤器拦截booleanmightContainBloomFilterUtil。localBloomFilter。mightContain(docId);if(!mightContain){是否有可能存在于布隆过滤器中log。info(selectfrombloomFilter,datanotavailable);returnnull;}StringredisKeydoc::info::docId;StringobjstringRedisTemplate。opsForValue()。get(redisKey);DocumentInfodocumentInfonull;if(StrUtil。isNotEmpty(obj)){log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{log。info(selectfromdb);documentInfothis。lambdaQuery()。eq(DocumentInfo::getId,docId)。one();if(ObjectUtil。isNotNull(documentInfo)){stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5L,TimeUnit。SECONDS);}}returndocumentInfo;}
  自定义分布式版布隆过滤器
  自定义分布式布隆过滤器的存储依赖于redis的bitmap数据结构来实现,另外还需要定义四个参数,分别为预估数据量size,误判率fpp,数组大小bitNum以及hash函数个数hashNum其中预估数据量和误判率需要配置在yml文件中。ResourceprivateStringRedisTemplatestringRedisTemplate;Value({bloom。filter。size})privatelongsize;预估数据量Value({bloom。filter。fpp})privatedoublefpp;误判率privatelongbitNum;数组大小lenprivateinthashNum;hash函数个数size
  根据上面的两个公式,计算出相应的数组长度以及所需的hash函数个数,并在redis设置一个布隆过滤器的key。计算出数组长度,hash函数个数并初始化数组PostConstructprivatevoidinitBloom(){this。bitNumgetNumOfBits(size,fpp);this。hashNumgetNumOfHashFun(size,bitNum);借助redis的bitmap来实现二进制数组stringRedisTemplate。opsForValue()。setBit(bloom::filter,bitNum,false);}计算bit数组大小paramsizeparamfppreturnprivatelonggetNumOfBits(longsize,doublefpp){return(long)(sizeMath。log(fpp)(Math。log(2)Math。log(2)));}计算所需的hash个数paramsizeparamnumOfBitsreturnprivateintgetNumOfHashFun(longsize,longnumOfBits){returnMath。max(1,(int)Math。round((double)numOfBitssizeMath。log(2)));}
  另外,需要提供两个方法,分别为添加元素的putBloomFilterRedis方法和判断元素是否有可能存在的方法existBloomFilterRedis,其中的实现方式参考了guava。像自定义布隆过滤器中添加元素paramkeypublicvoidputBloomFilterRedis(Stringkey){longhash64HashUtil。metroHash64(key。getBytes());inthash1(int)hash64;inthash2(int)(hash6432);for(inti1;ihashNum;i){上面不是说,要使用n个hash函数吗??为啥这里直接用一个动态变量取乘积了呢???不用担心,请看《LessHashing,SamePerformance:BuildingaBetterBloomFilter》,里面论述了这种操作不会影响布隆过滤器的性能,毕竟hash的代价还是很大的,这算是个有效的优化手段吧:Astandardtechniquefromthehashingliteratureistousetwohashfunctionsh(x)andh(x)tosimulateadditionalhashfunctionsoftheformg(x)h(x)ih(x)。intcombinedHashhash1ihash2;if(combinedHash0){如果为负数,则取反(保证结果为正数)combinedHashcombinedHash;}计算出数组下标,并将下标值置为1intbitIdx(int)(combinedHashbitNum);stringRedisTemplate。opsForValue()。setBit(bloom::filter,bitIdx,true);}}判断自定义布隆过滤器中元素是否有可能存在paramkeyreturnpublicbooleanexistBloomFilterRedis(Stringkey){longhash64HashUtil。metroHash64(key。getBytes());inthash1(int)hash64;inthash2(int)(hash6432);for(inti1;ihashNum;i){intcombinedHashhash1ihash2;if(combinedHash0){combinedHashcombinedHash;}intbitIdx(int)(combinedHashbitNum);判断下标值是否为1,如果不为1直接返回falseBooleanbitstringRedisTemplate。opsForValue()。getBit(bloom::filter,bitIdx);if(!bit){returnfalse;}}returntrue;}
  方法实现后,将所有的key值数据同步到redis中。ComponentpublicclassBloomFilterInitData{ResourceprivateBloomFilterUtilbloomFilterUtil;ResourceprivateDocumentInfoServicedocumentInfoService;PostConstructpublicvoidinitDocumentData(){ListDocumentInfodocumentInfosdocumentInfoService。lambdaQuery()。select(DocumentInfo::getId)。list();if(CollUtil。isNotEmpty(documentInfos)){documentInfos。stream()。map(m{returndoc::info::m。getId()。intValue();})。forEach(e{bloomFilterUtil。putBloomFilterRedis(e);});}}}
  上面全部搞定后,启动项目并测试结果是否有效,在启动前先在数据表中搞几条测试数据。
  OverridepublicDocumentInfogetDocumentDetail(intdocId){StringredisKeydoc::info::docId;布隆过滤器中是否有可能存在这个keybooleanbbloomFilterUtil。existBloomFilterRedis(redisKey);if(!b){如果不存在,直接返回空log。info(selectfrombloomFilter,datanotavailable);returnnull;}StringobjstringRedisTemplate。opsForValue()。get(redisKey);DocumentInfodocumentInfonull;if(StrUtil。isNotEmpty(obj)){log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{log。info(selectfromdb);documentInfothis。lambdaQuery()。eq(DocumentInfo::getId,docId)。one();if(ObjectUtil。isNotNull(documentInfo)){stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5L,TimeUnit。SECONDS);}}returndocumentInfo;}
  查询存在的数据。
  查询不存在的数据。
  查看两次请求打印的log,不存在的数据成功被拦截掉了,避免再去查询数据库,即使存在一定的误判率,也几乎不会有啥影响,最多就是查询一次数据库。
  虽然布隆过滤器可以有效的解决缓存穿透问题,并且实现的算法查找效率也很快。但是,也存在一定的缺点,由于存在hash冲突的原因,一方面存在一定的误判率(某个在过滤器中并不存在的key,但是通过hash计算出来的下标值都为1)。另一方面,删除比较困难(如果将一个数组位置为0,那么这个位置有可能也代表其他key的值,会影响到其他的key)。缓存击穿
  缓存击穿是指访问某个热点数据时,缓存中并不存在该数据或者缓存过期了,这个时候全部的请求压力给到了数据库。
  基于上面的代码,来模拟短时间内进行并发请求,看看会不会将请求全部打到数据库。publicstaticvoidmain(String〔〕args)throwsInterruptedException{短时间内并发请求接口,并访问同一个数据ExecutorServiceexecutorServiceExecutors。newFixedThreadPool(1000);CountDownLatchcountDownLatchnewCountDownLatch(1000);for(inti0;i1000;i){executorService。execute((){HttpResponseresponseHttpUtil。createGet(http:127。0。0。1:8081docqueryById?docId1)。execute();System。out。println(response。body());countDownLatch。countDown();});}countDownLatch。await();executorService。shutdown();}
  根据日志输出结果显示,请求确实给到了数据库。
  针对缓存击穿问题,有两种解决方案,一种是对热点数据不设置过期时间,另一种是采用互斥锁的方式。解决方案一:热点数据不设置过期时间
  热点数据不设置过期时间,当后台更新热点数据数需要同步更新缓存中的数据,这种解决方式适用于不严格要求缓存一致性的场景。解决方案二:使用互斥锁
  如果是单机部署的环境下可以使用synchronized或lock来处理,保证同时只能有一个线程来查询数据库,其他线程可以等待数据缓存成功后在被唤醒,从而直接查询缓存即可。如果是分布式部署,可以采用分布式锁来实现互斥。ComponentpublicclassRedisLockUtil{ResourceprivateStringRedisTemplatestringRedisTemplate;模拟互斥锁paramkeyparamvalueparamexpreturnpublicbooleantryLock(Stringkey,Stringvalue,longexp){BooleanabsentstringRedisTemplate。opsForValue()。setIfAbsent(key,value,exp,TimeUnit。SECONDS);if(absent){returntrue;}returntryLock(key,value,exp);如果线程没有获取锁,则在此处循环获取}释放锁paramkeyparamvaluepublicvoidunLock(Stringkey,Stringvalue){StringsstringRedisTemplate。opsForValue()。get(key);if(StrUtil。equals(s,value)){避免锁被其他线程误删stringRedisTemplate。delete(key);}}}
  有了上面的两个方法,可以对业务代码进行改造,在查询数据库前进行加锁,读取完成后在释放锁。OverridepublicDocumentInfogetDocumentDetail(intdocId){StringredisKeydoc::info::docId;booleanbbloomFilterUtil。existBloomFilterRedis(redisKey);if(!b){log。info(selectfrombloomFilter,datanotavailable);returnnull;}StringobjstringRedisTemplate。opsForValue()。get(redisKey);DocumentInfodocumentInfonull;if(StrUtil。isNotEmpty(obj)){log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{StringsUUID。randomUUID()。toString();给锁加个标识,避免误删StringlockKeyredisKey::lock;booleanlockredisLockUtil。tryLock(lockKey,s,60);尝试加锁if(lock){try{如果加锁成功,先再次查询缓存,有可能上一个线程查询并添加到缓存了objstringRedisTemplate。opsForValue()。get(redisKey);if(StrUtil。isNotEmpty(obj)){log。info(selectfromcache);documentInfoJSONUtil。toBean(obj,DocumentInfo。class);}else{log。info(selectfromdb);documentInfothis。lambdaQuery()。eq(DocumentInfo::getId,docId)。one();if(ObjectUtil。isNotNull(documentInfo)){stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5L,TimeUnit。SECONDS);}}}finally{redisLockUtil。unLock(lockKey,s);释放锁}}}returndocumentInfo;}
  一顿梭哈后,再次模拟并发查询,看看最终效果,理想的结果状态应该是查询一次数据库,后面的查询直接通过缓存获取。通过日志输出可以看出来,击穿问题被有效解决啦。
  缓存雪崩
  缓存雪崩是指对热点数据设置了相同的过期时间,在同一时间这些热点数据key大批量发生过期,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。与缓存击穿不同的是,缓存击穿是单个热点数据过期,而缓存雪崩是大批量热点数据过期。
  针对缓存雪崩问题,常见的解决方案有多种,比如设置随机的过期时间或者不设置过期时间,搭建高可用的缓存架构避免redis服务宕机,服务降级等。解决方案一:设置随机的过期时间
  将key的过期时间后面加上一个随机数,这个随机数值的范围可以根据自己的业务情况自行设定,这样可以让key均匀的失效,避免大批量的同时失效。if(ObjectUtil。isNotNull(documentInfo)){生成一个随机数intrandomIntRandomUtil。randomInt(2,10);过期时间随机数stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo),5LrandomInt,TimeUnit。SECONDS);}解决方案二:不设置过期时间
  不设置过期时间时,需要注意的是,在更新数据库数据时,同时也需要更新缓存数据,否则数据会出现不一致的情况。这种方式比较适用于不严格要求缓存一致性的场景。if(ObjectUtil。isNotNull(documentInfo)){stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo));}解决方案三:搭建高可用集群
  缓存服务故障时,也会触发缓存雪崩,为了避免因服务故障而发生的雪崩,推荐使用高可用的服务集群,这样即使发生故障,也可以进行故障转移。数据一致性
  通常情况下,使用缓存的直接目的是为了提高系统的查询效率,减轻数据库的压力。一般情况下使用缓存是下面这几步骤:查询缓存,数据是否存在如果数据存在,直接返回如果数据不存在,再查询数据库如果数据库中数据存在,那么将该数据存入缓存并返回。如果不存在,返回空。
  这么搞好像看上去并没有啥问题,那么会有一个细节问题:当一条数据存入缓存后,立刻又被修改了,那么这个时候缓存该如何更新呢。不更新肯定不行,这样导致了缓存中的数据与数据库中的数据不一致。一般情况下对于缓存更新有下面这几种情况:先更新缓存,再更新数据库先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存先更新缓存,再更新数据库
  先更新缓存,再更新数据库这种情况下,如果业务执行正常,不出现网络等问题,这么操作不会有啥问题,两边都可以更新成功。但是,如果缓存更新成功了,但是当更新数据库时或者在更新数据库之前出现了异常,导致数据库无法更新。这种情况下,缓存中的数据变成了一条实际不存在的假数据。
  比如,在更新文章详情时,先修改了redis中的数据,在更新数据库前抛出一个异常来模拟数据库更新失败的场景。牛逼啊!接私活必备的N个开源项目!赶快收藏publicbooleanupdateDocument(DocumentInfodocumentInfo){StringredisKeydoc::info::documentInfo。getId();先更新缓存stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo));模拟更新数据库前出现异常inti10;在更新数据库booleanbthis。updateById(documentInfo);returnb;}PostMapping(docupdateDocument)publicResultBooleanupdateDocument(RequestBodyDocumentInfodocumentInfo){returnResult。success(documentInfoService。updateDocument(documentInfo));}
  先调用更新的接口,在调用查询的接口,结果发现查询的接口返回的并不是数据库中的实际数据,这个时候就造成了缓存与数据库数据不一致的情况。
  先更新数据库,再更新缓存
  先更新数据库,再更新缓存和先更新缓存,再更新数据库的情况基本一致,如果失败,会导致数据库中是最新的数据,缓存中是旧数据。还有一种极端情况,在高并发情况下容易出现数据覆盖的现象:A线程更新完数据库后,在要执行更新缓存的操作时,线程被阻塞了,这个时候线程B更新了数据库并成功更新了缓存,当B执行完成后线程A继续向下执行,那么最终线程B的数据会被覆盖。
  OverridepublicbooleanupdateDocument(DocumentInfodocumentInfo){StringredisKeydoc::info::documentInfo。getId();更新数据库booleanbthis。updateById(documentInfo);以标题为标识,模拟线程阻塞。当一个请求的标题为‘模拟数据覆盖’时,线程停4秒if(StrUtil。equals(documentInfo。getTitle(),模拟数据覆盖)){try{Thread。sleep(4000);}catch(Exceptione){}}更新缓存stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo));模拟更新数据库前出现异常returnb;}先删除缓存,再更新数据库
  先删除缓存,再更新数据库这种情况,如果并发量不大用起来不会有啥问题。但是在并发场景下会有这样的问题:线程A在删除缓存后,在写入数据库前发生了阻塞。这时线程B查询了这条数据,发现缓存中不存在,继而向数据库发起查询请求,并将查询结果缓存到了redis。当线程B执行完成后,线程A继续向下执行更新了数据库,那么这时缓存中的数据为旧数据,与数据库中的值不一致。
  先更新数据库,再删除缓存
  先更新数据库,再删除缓存也并不是绝对安全的,在高并发场景下,如果线程A查询一条在缓存中不存在的数据(这条数据有可能过期被删除了),查询数据库后在要将查询结果缓存到redis时发生了阻塞。这个时候线程B发起了更新请求,先更新了数据库,再次删除了缓存。当线程B执行成功后,线程A继续向下执行,将查询结果缓存到了redis中,那么此时缓存中的数据与数据库中的数据发生了不一致。
  解决数据不一致方案延时双删
  延时双删,即在写数据库之前删除一次,写完数据库后,再删除一次,在第二次删除时,并不是立即删除,而是等待一定时间在做删除。
  这个延时的功能可以使用mq来实现,这里为了省事,偷个懒,本地测试使用的延时队列来模拟mq达到延时效果。首先需要定义一个队列元素对象DoubleDeleteTask。DatapublicclassDoubleDeleteTaskimplementsDelayed{privateStringkey;需要删除的keyprivatelongtime;需要延迟的时间publicDoubleDeleteTask(Stringkey,longtime){this。keykey;this。timetimeSystem。currentTimeMillis();}OverridepubliclonggetDelay(TimeUnitunit){returnunit。convert(timeSystem。currentTimeMillis(),TimeUnit。MILLISECONDS);}OverridepublicintcompareTo(Delayedo){returnLong。compare(time,((DoubleDeleteTask)o)。time);}}
  然后定义一个队列并交给spring管理。ConfigurationpublicclassDoubleDeleteQueueConfig{Bean(namedoubleDeleteQueue)publicDelayQueueDoubleDeleteTaskdoubleDeleteQueue(){returnnewDelayQueueDoubleDeleteTask();}}
  设置一个独立线程,特意用来处理延时的任务。如果数据删除失败,可以自定义重试次数以保证数据的一致性,但是也会带来一定的性能影响,如果在实际项目中,建议还是以异步的方式来实现重试。Slf4jComponentpublicclassDoubleDeleteTaskRunnerimplementsCommandLineRunner{ResourceprivateDelayQueueDoubleDeleteTaskdoubleDeleteQueue;ResourceprivateStringRedisTemplatestringRedisTemplate;privatestaticfinalintretryCount3;失败重试次数Overridepublicvoidrun(String。。。args)throwsException{newThread((){try{while(true){DoubleDeleteTasktakedoubleDeleteQueue。take();取出队列元素Stringkeytake。getKey();try{stringRedisTemplate。delete(key);log。info(延时删除key:{},key);}catch(Exceptione){失败重试intcount1;for(inti1;iretryCount;i){if(countretryCount){log。info(延时删除key:{},失败重试次数:{},key,count);BooleanrstringRedisTemplate。delete(key);if(r){break;}else{count;}}elsebreak;}}}}catch(Exceptione){e。printStackTrace();}},doubledeletetask)。start();}}
  使用延时队列,处理延时双删。最终测试,通过日志的打印可以确定实现了延时删除的功能。OverridepublicbooleanupdateDocument(DocumentInfodocumentInfo){StringredisKeydoc::info::documentInfo。getId();更新缓存stringRedisTemplate。opsForValue()。set(redisKey,JSONUtil。toJsonStr(documentInfo));更新数据库booleanbthis。updateById(documentInfo);再次延时删除缓存doubleDeleteQueue。add(newDoubleDeleteTask(redisKey,2000L));returnb;}
  最后
  在高并发的场景下,使用reids还是存在很多坑的,稍不注意就会出现缓存穿透,缓存雪崩等情况,严重的话可以直接造成服务宕机,所以在以后的开发中需要注意(如果项目没啥并发量的话,可以不用考虑)。
  原文链接:juejin。cnpost7185923117611483196

人生,越过越顺,从自愈开始文雪落无尘人的一生会经历很多事情,有的会成为你前进的动力,而有的会伤害你。树木被刀子割了以后会自愈,这说明树的顽强。同样,人在被伤害后,如果能够拥有自愈力,才是最优……容易令人爱不释手的小机器,山灵M0pro把玩分享其实在各种听音的环境下,我一直热衷于各司其职般的场景划分,例如在桌面上我基本都是用台机来接驳大耳,在客厅就瘫在沙发上听听音箱,出门基本就TWS或小尾巴,晚上睡觉助眠时就成了随身……不明原因的儿童肝炎病例还在增加!专家提醒一定要做好这件事嗨!小伙伴们,我是聊健康的小米毛线球,简单健康,让健康简单一点。最近,17个国家均发生了不明原因的儿童肝炎,其中有一部分病例发病后非常严重,必须立即进行肝移植。这让咱们国……大反转,C罗竟遭争抢!滕哈格为他辩解,爵爷施压,不让加盟对手最近曼联再次成为球迷关注焦点,主要还是C罗回归之后首场热身赛居然就和滕哈格发生了矛盾。随后滕哈格也没有惯着这位葡萄牙巨星,在记者采访中谈到:任何人都不能够高于团队,C罗的提前离……像点外卖一样点iPhone14闪购时代来了文董二千如果消费者在20年前想要买一部手机,那他应该只能去线下门店购买;如果是在10年前,那他便可以选择在电商下单不过要比线下购买多等几天。而现在,线下门店和电商开始融合……北京延庆苍鹭群在白河堡水库筑巢原标题:北京延庆:苍鹭群在白河堡水库筑巢连日来,北京市延庆区白河堡水库内的悬崖峭壁上,迎来了数十只苍鹭筑巢孵卵、栖息觅食。白河堡水库被誉为燕山天池,春日里碧水悠悠。……人类在寻找外星人,科学家29个行星的外星人,可能也在观察地球人类在走出地球之后,看到了一个浩瀚的宇宙,我们在感叹宇宙宏伟的同时,也在思考一个共同的问题:外星人存在吗?如果你认为外星人不存在,那你的认知就跟井底之蛙一样,几乎所有的科……副部级迟京涛工作横跨两家央企,与宁高宁搭档,参与蒙牛收购据公开报道,已年满60周岁的迟京涛今日被免职,按照有关程序办理退休。迟京涛迟京涛,原中粮集团董事、总经理、党组副书记(副部级)。今天宣布退休前他已经在中粮集团二把手……张庆鹏生涯最后一战有多强?殳海除了科比退役战没见过这样的人你看看,就是到退役那天下半场然后得了37分。除了科比退役之战,我没有见过有个人退役之战打成这样的。殳海在最近一期的《CBA挖掘机》节目当中说。哎,这说的是谁?熟悉CBA、……baby旧照曝光!胶原蛋白满满美貌似洋娃娃baby旧照曝光!胶原蛋白满满美貌似洋娃娃baby旧照曝光!胶原蛋白满满美貌似洋娃娃baby旧照曝光!胶原蛋白满满美貌似洋娃娃baby旧照曝光!胶原蛋白满满美……X14ProMax发布!价格1399元竟然和小米相似?从iPhone4S开始,国内的很多厂商都会去借鉴苹果手机的外观设计,有的是借鉴外观、有的是借鉴刘海、有的是借鉴直角边框!就比如手机历史上出现的尼彩手机、还有各种各样奇奇怪……王者荣耀12。24体验服更新8位英雄平衡调整后羿群控加强大家好,我是七七。王者荣耀24日的体验服更新,对8位英雄进行了调整,其中裴擒虎竟然是进行了全新重做,而之前一直在调整的达摩、后羿也依数在列。那么具体是怎么一个调整法呢?让我们一……
文章被抄袭了不可怕,看看曹老板的宽容大度,你不也成名人了吗?三国杀移动版三国手杀哪家强,且看小锋来拆墙。各位客官老爷们大家好啊,小锋又和您见面了。前些天,小锋无意间发现自己的文章疑似被人抄袭了。虽然说我们这些作者写出来的文章……每日智讯22122113代酷睿i513400F提前上架i512400F可以说是颇受主流用户青睐的型号,它的继任者i513400F也不远了。美国电商ShopBLT已经公布了新品的价格,i513……安徽岳西春到大别山绿水绕青山在安徽省安庆市岳西县毛尖山乡林河村,各类植物开始抽新吐绿,春意盎然。吴均奇摄层层叠叠的山峦与错落有致的民居、通村达户的公路相映成景,绘就一幅春日唯美画卷。吴均奇摄春……女足天花板!23岁美女加盟大连,笑靥如花纤腰美腿,完胜网红明女足在世界足坛发展十分迅速,尤其是国内近些年成绩崛起,加上足协也在扶持,所以女足的关注度变得越来越高。目前为止已经有多达10多名女足球员留洋海外,成为亚洲范围内留洋球员最多的女……宇宙五大奇观,每一个都让我们感到惊奇,感叹宇宙的美丽在很多人的眼里,宇宙黑暗而寂静,看不到什么美丽的景色,可事实上,在天文望远镜的镜头里,黑暗的宇宙背后却是一个美丽的精彩世界。宇宙浩瀚而广阔,它带给我们的直观体验就是黑暗而……养生堂名医的春季养生法点击蓝字关注我们嘉宾王国玮:首都名中医北京中医医院原副院长关键词肝火春季养生长寿是每个人的追求目标,想要……一个退役操盘手对散户的良心忠告,大道至简美国康乃尔大学的两位科学家曾做过一个有趣试验:他们在两个玻璃瓶里各放进5只苍蝇和5只蜜蜂。然后将玻璃瓶的底部对着有亮光的一方,而将开口朝向暗的一方。几个小时之后,科学家发……NothingPhone(1)手机推送安卓13NothingIT之家1月14日消息,NothingPhone(1)手机在大约一个月前获得了首个基于Android13的NothingOS1。5Beta。现在,Nothing正在向加入该计划……埃隆马斯克母亲才是我的英雄,我的成功都来自她的培养和影响2021年9月28日福布斯实时富豪榜,特斯拉及SpaceX首席执行官埃隆马斯克,以2034亿美元身价,超越亚马逊CEO贝索斯,重登世界首富,连续霸榜至今。2022上半年财……新鲜早科技丨腾讯XR暂停部分业务美国线上二手车商Carvan21世纪经济报道数字经济课题组综合报道早上好,新的一周又开始了。在过去的这个周末内,科技行业发生了哪些有意思的事情?来跟21tech一起看看吧。【巨头风向标】……联想反击不是舆论战而是产品,联想摩托罗拉新款手机炸街作为一款旗舰级的机型,摩托罗拉edgeS30可谓是将性价比诠释得相当到位,而这样一颗搭载了骁龙888Plus的旗舰,官方公布的起售价却仅有1799元。摩托罗拉edgeS3……暗黑纹身这20款真的惊艳!头条创作挑战赛现在很多年轻人都喜欢暗黑纹身,因为它很酷。但是有一些人认为暗黑纹身就是在显示他们的黑恶side。我不同意这个看法。我认为暗黑纹身是一种艺术形式,它可以……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网