redis-缓存-双写一致性
redis-缓存-双写一致性
- 一、来因宫
- 1.1、案件情景再现
- 1.2、案件分析
- 二、上手段
- 2.1、删除-存储-延迟删除
- 2.2、加锁
- 2.3、异步通知
一、来因宫
1.1、案件情景再现
双写一致:保持Reids和Mysql数据在修改的时的一致性;
请看案例~!
老板:“小李,下周搞个活动,把app首页的轮播图换一下~”
小李:”收到”
小李开始忙乎起来,登录后台,准备替换新的活动轮播图,几分钟的时间检查无误后就搞定发布了小李:”换好了,老板”
老板:”你换到哪里去了?怎么还是原来的图!”
老板气冲冲的拿着手机指着屏幕对小李说道
小李看着手机上显示的旧图,十分委屈,自己明明已经替换过了!怎么没有更新?隔壁的秃顶老王听到两人吵闹声,默默的打开了IDEA
// 1. 构建缓存KeyString cacheKey = BANNER_CACHE_KEY_PREFIX + updatedBanner.getId();// 2. 删除Redis中需要更新的Banner缓存Boolean deleted = redisTemplate.delete(cacheKey);if (deleted != null && deleted) {System.out.println("成功删除Redis中Banner缓存: " + cacheKey);} else {System.out.println("Redis中不存在该Banner缓存或删除失败: " + cacheKey);}// 3. 将更新后的数据同步到MySQLBanner savedBanner = bannerRepository.save(updatedBanner);System.out.println("Banner数据已成功更新到MySQL: " + savedBanner.getId());
紧张的汗水从老王的头顶冒了出来,心中暗自琢磨:这代码怎么了?有啥问题么?先删除,在插入数据库....用户访问app再去查询mysql随后更新到redis没问题啊!
老王猛的睁大眼睛,悄咪咪的抹掉头上的汗水
“咳咳,老板你别急,小李你再重新操作下,刚才监控到有网络延迟,可能是没有同步过去”
老王转过身在小李一侧说道
小李听到老王的话,又重新操作了一遍,亲自打开手机先看一下,果然新的轮播图已经出现了!
“哇,王哥,你真厉害~果然可以了”
小李拿着手机摇晃的说道
“赶紧干活!”
老板重新点进去看轮播图已经更新,说了一句话就走开了
“王哥,你是好人,写信出,请你喝杯新品雪王!嘻嘻”小李笑呵呵的对老王说道
看的老王一愣一愣的,尴尬的在那抓头
1.2、案件分析
下班后
“老王,下班了,还不走” 准备下班的老板看着坐在工位上的老王问了句
“啊,老板,今天网络延迟问题,我等等看能不能复现看看哪里问题”老王眼珠子一转说道
“哈哈,当初招你进来,就看你靠谱,好好干!”老板说了声转头就走了
......
老王喝着小李妹妹雪王新品,默默戴上耳机,盯着屏幕上的代码,双手飞快的敲击着!
"要不是app日活达到了20多人,这个问题可能还真暴漏不出来!"老王小声的嘀咕着
*麦芒掉进针眼里--巧极了!*
*小李妹妹发布的时候,程序刚刚把redis缓存的数据删除,程序还没有往下执行!正好有人访问了app,*
*查询的逻辑又开始了!查到缓存没有数据,又从还未更新的mysql查询放到缓存了!*
导致发布完成后轮播图缓存还是旧的!
二、上手段
2.1、删除-存储-延迟删除
"看来流量大了也不是好事啊!耽误俺老王下班!"
......
*“那就让轮播图放入数据库成功之后,再把缓存删除一遍!”*
*“我可真是个机灵鬼!嘿嘿嘿”*
/*** 更新Banner数据,保证Redis和MySQL一致性* 采用先删缓存、更新数据库、再删缓存的策略*/@Transactional(rollbackFor = Exception.class)public Banner updateBanner(Banner banner) {if (banner.getId() == null) {throw new IllegalArgumentException("Banner ID不能为空");}// 1. 先删除Redis中的旧缓存String cacheKey = BANNER_CACHE_KEY_PREFIX + banner.getId();redisTemplate.delete(cacheKey);System.out.println("第一次删除Redis缓存: " + cacheKey);try {// 2. 查询数据库中是否存在该BannerBanner existingBanner = bannerRepository.findById(banner.getId()).orElseThrow(() -> new RuntimeException("Banner不存在,ID: " + banner.getId()));// 3. 更新Banner数据existingBanner.setTitle(banner.getTitle());............// 4. 保存更新到数据库Banner updatedBanner = bannerRepository.save(existingBanner);System.out.println("Banner数据已更新到数据库,ID: " + updatedBanner.getId());// 5. 再次删除Redis缓存,防止并发场景下的缓存脏数据redisTemplate.delete(cacheKey);System.out.println("第二次删除Redis缓存: " + cacheKey);return updatedBanner;} catch (Exception e) {// 发生异常时可以考虑记录日志,进行补偿操作等System.err.println("更新Banner失败: " + e.getMessage());throw e;}}
"大功告成!搞定,我真是个小天才,哈哈"老王看着自己的代码欣赏的说道!
*"这他娘的,数据库现在用的主从模式,同步也需要时间啊!直接删除了,万一删除完,*
*另外的节点有查询还是旧数据啊!"*
*"难不住俺!延迟一会再删。桀桀桀"*
public void delayDeleteCache(String cacheKey, long delay) {try {// 延迟指定时间TimeUnit.MILLISECONDS.sleep(delay);// 第二次删除缓存redisTemplate.delete(cacheKey);System.out.println("延迟删除缓存成功: " + cacheKey);} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("延迟删除缓存失败: " + e.getMessage());}}
2.2、加锁
"这要延迟多久呢?我咋知道它啥时候能同步完数据了呢?"
“不行,还得再改改”
老王拿出抽屉钥匙🔑,从中拿出一包破旧烟盒,拿出一根邹邹巴巴的烟点上,叼在嘴里
将烟和打火机放进抽屉,上锁拔掉钥匙
“叮、叮、叮”
老王拿着钥匙在桌面上有节奏的敲击着
“有了!上锁啊!”
老王猛吸一口,将烟放在烟灰缸上
import com.example.entity.Banner;
import com.example.mapper.BannerMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.TimeUnit;@Service
public class BannerService {@Resourceprivate BannerMapper bannerMapper;@Resourceprivate RedisTemplate<String, Object> redisTemplate;// 读写锁:排他锁(写锁)和共享锁(读锁)private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 缓存键前缀private static final String BANNER_CACHE_KEY = "banner:";/*** 更新Banner数据* 使用排他锁,确保更新期间其他线程无法读取*/@Transactionalpublic Banner updateBanner(Banner banner) {// 获取写锁(排他锁)readWriteLock.writeLock().lock();try {// 1. 先删除Redis中的旧缓存String cacheKey = BANNER_CACHE_KEY + banner.getId();redisTemplate.delete(cacheKey);// 2. 更新数据库if (banner.getId() == null) {bannerMapper.insert(banner);} else {bannerMapper.updateById(banner);}// 3. 异步延迟删除缓存,解决主从同步延迟可能带来的问题asyncDelayDeleteCache(cacheKey, 1000); // 延迟1秒return banner;} finally {// 释放写锁readWriteLock.writeLock().unlock();}}/*** 查询Banner数据* 使用共享锁,允许多个线程同时读取*/public Banner getBannerById(Long id) {String cacheKey = BANNER_CACHE_KEY + id;// 获取读锁(共享锁)readWriteLock.readLock().lock();try {// 1. 先查缓存Banner banner = (Banner) redisTemplate.opsForValue().get(cacheKey);if (banner != null) {return banner;}// 2. 缓存未命中,查数据库banner = bannerMapper.selectById(id);if (banner != null) {// 3. 写入缓存,设置过期时间redisTemplate.opsForValue().set(cacheKey, banner, 30, TimeUnit.MINUTES);}return banner;} finally {// 释放读锁readWriteLock.unlock();}}/*** 异步延迟删除缓存*/@Asyncpublic void asyncDelayDeleteCache(String cacheKey, long delayMillis) {try {// 延迟指定时间Thread.sleep(delayMillis);// 再次删除缓存redisTemplate.delete(cacheKey);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}
共享锁readWriteLock.readLock().lock()
加锁后其他的线程还能继续读数据,不影响响应速度
排他锁readWriteLock.writeLock().lock()
加锁之后,其他的线程就不能执行了!得等它操作完,这样数据就一样了
"这下是没问题了,上了两把锁,绝对一致!" 老王乐滋滋
老王想起没抽完的香烟,低头一看,放在烟灰缸上的烟已经燃尽
老王拿起钥匙开锁 、拿烟、点燃、上锁
老王反常的皱起眉头:*“加锁安全了,但是这开锁、上锁的,有点影响时间啊!”*
2.3、异步通知
"轮播图也不是啥非得要求强一致性,直接延迟删除就行了呗,但是这方法有点..."
再想想!
“脑子不行了!查查网上有啥好解决办法没!”
在业务要求强一致性的情况下,例如涉及到金钱安全问题,那我们使用加锁是完全没有问题的;
像 其他热点缓存没有要求强一致性,就可以采用异步通知的方案进行解决
"还得是网上的大神们啊!这方法给写的板板正正,MQ消息中间件,正好公司有,研究研究"
"可以在处理逻辑中向MQ发送消息"
"监听到这个消息就会进行同步更新缓存"
老王上唇不自觉地向上提拉,脸颊的皮肤都绷成一张浅弓
随后随着悠长的 “哈 ——” 声
老王看了看时间,已经9点了!
“卧槽,赶紧回家,再晚一点赶不上末班车了!”
*cannel中间件咋用?快教教老王!!!*