当前位置: 首页 > news >正文

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;}}
"大功告成!搞定,我真是个小天才,哈哈"老王看着自己的代码欣赏的说道!

redis-mysql双写一致性、双写一致性

*"这他娘的,数据库现在用的主从模式,同步也需要时间啊!直接删除了,万一删除完,*
*另外的节点有查询还是旧数据啊!"*
*"难不住俺!延迟一会再删。桀桀桀"*
	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发送消息"
"监听到这个消息就会进行同步更新缓存"

MQ异步消息传输

老王上唇不自觉地向上提拉,脸颊的皮肤都绷成一张浅弓
随后随着悠长的 “哈 ——” 声
老王看了看时间,已经9点了!
“卧槽,赶紧回家,再晚一点赶不上末班车了!”

在这里插入图片描述

*cannel中间件咋用?快教教老王!!!*
http://www.dtcms.com/a/342405.html

相关文章:

  • 【Django:基础知识】
  • 掌控不平等的力量:深入解析帕雷托分布与二八法则的数学内核
  • python测试开发django-1.开始hello world!
  • 《零基础入门AI:深度学习之NLP基础学习》
  • 在Python中, list相减 要从一个列表(valid_points)中排除另一个列表(yuanjian_jiaodian)的所有元素
  • Linux CentOS 安装 .net core 3.1
  • 银河麒麟V10系统离线安装zabbix-agent教程
  • 18维度解密·架构魔方:一览无遗的平衡艺术
  • nginx-重定向-正则表达式-路由匹配优先级
  • Qt截图工具项目开发教程 - 从零开始构建系统截图工具
  • 【ARM】Keil MDK如何指定单文件的优化等级
  • 牛津大学xDeepMind 自然语言处理(5)
  • 基于 Kubernetes 的 WordPress 网站部署(使用 ConfigMap)
  • Spring两个核心IoCDI(一)
  • javaweb开发笔记—— 前端工程化
  • 当安全遇上资源瓶颈:轻量级加密为何成为 IoT 时代的刚需?
  • 基于 FPGA 的电磁超声脉冲压缩检测系统
  • 家里Windows,公司Linux?通过cpolar,WSL开发环境无缝切换
  • Python数据可视化利器:Matplotlib从入门到实战全解析
  • 今天我们继续学习计算机网络技术,Cisco软件,三层交换机以及RIP动态协议
  • 从零开始:JDK 在 Windows、macOS 和 Linux 上的下载、安装与环境变量配置
  • DeepSeek R2难产:近期 DeepSeek-V3.1 发布,迈向 Agent 时代的第一步
  • 《杠杆》电视剧分析学习
  • 【python与生活】如何从视频中提取关键帧?
  • JAVA-15 (2025.08.20学习记录)
  • 数据库面试常见问题
  • 【OpenGL】LearnOpenGL学习笔记13 - 深度测试、模板测试
  • 05 ODS层(Operation Data Store)
  • LeetCode算法日记 - Day 18: 只出现一次的数字、只出现一次的数字III
  • 通信工程学习:什么是Template Matching模版匹配