聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考1

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考

写文一方面是为了自己复习面试,另外就是想让大家和我一起卷吧(哈哈说笑了,是希望大家有所收获吧)

我也不敢说什么一文详解本地缓存和分布式缓存,本文就是针对事实,用图文并茂的方式,可运行的代码案例结合常见的面试题,一点点的推导和分析发生的问题。

文章中针对可能会出现的问题,都附带了一副彩图,来让大家对问题具有更深刻的理解记忆

希望各位小伙伴都能够满载而归~

全文大致9000字上下,并且具有推导性,所以在看本文前,建议能腾出较多的空闲时间,摸鱼看的话,可能会略微有一点点割裂感,可以先收藏,哈哈~

文章大纲:

image.png

一、缓存

1、缓存的基本知识

1)什么是缓存?

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而DB只承担数据落盘工作。

最简单的理解就是缓存是挡在 DB 前面的一层,为DB遮风挡雨。

架构中最经典的一句话:“没有什么是加一层不能解决的,如果加一层不行,就再加一层”。

2)什么样的数据适合放入缓存?

精简为四字就是:读多写少

  • 访问量很大,需要使用缓存来承担一部分压力(读多、写少)
  • 即时性要求高,能承受一定时间内的数据不一致性。
  • 较长时间不会改变的数据,如后台管理的菜单列表,商品分类列表等等。

3)使用缓存后会产生什么样的问题?

  • 缓存与数据库双写不一致
  • 缓存雪崩、缓存穿透
  • 缓存并发竞争

4)缓存的使用流程

image.png

这只是一个非常简单的流程介绍,实际中还是有不少值得思考的地方。

2、使用Map模拟本地缓存

所谓的缓存,其实就是一个位于应用程序与数据库之间的一层,作用就是减少访问数据库的次数,以提高服务性能。

单机服务下,一些较小,并且是单线程中用到的到数据,使用本地 Map 来存储也不是不可以。

如果是学习过 ThreadLocal的小伙伴,就可能见过ThreadLocalMap 这个Map,一般而言,ThreadLocal都是用来存储本次请求中一些信息(例如:当前请求中登录用户信息),方便在整个请求过程中使用,不过往往它都是一次性的~

我下面的案例只是简单的模拟一下本地缓存,并不实用,为解释大致的含义而写。

 /**
  * <p>
  * 分类菜单 服务实现类
  * </p>
  *
  * @author Ning Zaichun
  * @since 2022-09-07
  */
 @Slf4j
 @Service
 public class LocalMenuServiceImpl implements ILocalMenuService {
 ​
     /**
      * 本地缓存
      * 最开始的话,拿 HashMap 模拟
      * 但 HashMap 它是一个非线程安全类集合,
      * 进一步又改为使用 ConcurrentHashMap,多线程下安全的集合
      */
     private Map<String, Object> localCacheMap = new ConcurrentHashMap<String, Object>();
 ​
     private static final String LOCAL_MENU_CACHE_KEY = "local:menu:list";
 ​
     @Autowired
     private MenuMapper menuMapper;
 ​
 ​
     @Override
     public List<MenuEntity> getLocalList() {
         //1、判断本地缓存中是否存在
         List<MenuEntity> menuEntityList = (List<MenuEntity>) localCacheMap.get(LOCAL_MENU_CACHE_KEY);
         //2、本地缓存中有,就从缓存中拿
         if (menuEntityList == null) {
             //3、如果缓存中没有,就重新查询数据库
             log.info("缓存中没有,查询数据库,重新构建缓存");
             menuEntityList = menuMapper.selectList(new QueryWrapper<MenuEntity>());
             //4、从数据库查询到结果后,重新放入缓存中
             localCacheMap.put(LOCAL_MENU_CACHE_KEY, menuEntityList);
             return menuEntityList;
         }
         log.info("缓存中有直接返回");
         //5、将结果返回
         return menuEntityList;
     }
 ​
     /**
      * 更新操作
      *
      * @param menu
      * @return
      */
     @Override
     public Boolean updateLocalMenuById(MenuEntity menu) {
         //1、删除本地缓存数据
         localCacheMap.remove(LOCAL_MENU_CACHE_KEY);
         System.out.println("清空本地缓存===>");
         //2、更新数据库,根据id更新数据库中实体信息
         return menuMapper.updateById(menu) > 0;
     }
 ​
 }

问题:

并不实用,存在较多问题,存储数据量较小,并发能力较弱等等。


软件架构中一直流传着这么一句话:

"没有什么是加一层解决不了的,如果加一层解决不了,就再加一层"。

所以就将缓存抽取出来,架构演变成如下图:

image.png

二、集成 Redis 做缓存 and Redis 的使用

这里准确点说应当是集成 Redis 做编程式的缓存,而非大家常见的集成 Spring-Cache 利用注解做缓存。

我从上至下大致会说到的下列几个知识点:

  • Redis 的简单使用
  • Redis 序列化机制
  • 更改 Redis 默认序列化机制
  • 使用 Redis 做编程式的缓存
  • 简单讲解了 Redis 的两个连接工厂 JedisLettuce

2.1、前期准备

添加 Redis 的相关依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

使用SpringBoot这么久以来,看到某个stater,可以放心的推测,它大概率会有一个xxxxAutoConfiguration的自动配置类

image.png

在这里可以看到它给我们自动注入了RedisTemplateStringRedisTemplate两个常用的操作模板类。

配置yml文件:

 spring:
   application:
     name: springboot-cache
   redis:
     host: 192.168.1.1
     password: 000415 #有就写,木有则省略

2.2、Redis 的简单使用

在说其他的之前,我们先来看看 Redis 常用的一些命令,从浅到深

 /**
  * @description:
  * @author: Ning Zaichun
  * @date: 2022年09月21日 22:01
  */
 @RunWith(SpringRunner.class)
 @SpringBootTest(classes = {ApplicationCache.class})
 public class RedisTest {
 ​
 ​
     @Autowired
     private StringRedisTemplate stringRedisTemplate;
 ​
     /**
      * set key value 命令 使用
      */
     @Test
     public void test1() {
         // set key value 往redis 中set 一个值
         stringRedisTemplate.opsForValue().set("username", "宁在春");
         // get key : 从redis中根据key 获取一个值
         System.out.println(stringRedisTemplate.opsForValue().get("username"));
         //out:宁在春
 ​
         // del key: 从redis 中删除某个key
         stringRedisTemplate.delete("username");
         System.out.println(stringRedisTemplate.opsForValue().get("username"));
     }
 ​
     /**
      * setnx  key value : 如果 key 不存在,则设置成功 返回 1 ;否则失败 返回 0
      */
     @Test
     public void test2() {
         Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1");
         System.out.println(lock);
         Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1");
         System.out.println(lock2);
         //true
         //false
     }
 ​
     /**
      * set  key value nx ex : 如果 key 不存在,则设置值和过期时间 返回 1 ;否则失败 返回 0
      * nx、ex 都为命令后面的参数
      * 更为详细命令的解释:https://redis.io/commands/set/
      * 这个命令也常常在分布式锁中出现,悄悄为后文铺垫一下
      */
     @Test
     public void test3() {
         Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock:nx:ex", "lock", 30L, TimeUnit.SECONDS);
         System.out.println(lock);
         Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent("lock:nx:ex", "lock", 30L, TimeUnit.SECONDS);
         System.out.println(lock2);
         //true
         //false
     }
 ​
 ​
     /**
      * 上述的三个测试,都是基础的 set 命令的,但 redis 中也有很多其他的数据结构,如 list、set、map 等等
      * list测试,可将列表视为队列(先进先出)
      * 想要详细了解,请查看:https://redis.io/docs/data-types/lists/
      * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】
      */
     @Test
     public void test4() {
         ListOperations<String, String> opsForList = stringRedisTemplate.opsForList();
         //从列表左边进行插入
         opsForList.leftPush("left:key", "1");
         opsForList.leftPush("left:key", "2");
         opsForList.leftPush("left:key", "3");
         opsForList.leftPush("left:key", "4");
         opsForList.leftPush("left:key", "5");
         //从列表右边进行插入
         opsForList.rightPush("left:key", "10");
         opsForList.rightPush("left:key", "9");
         opsForList.rightPush("left:key", "8");
         opsForList.rightPush("left:key", "7");
         opsForList.rightPush("left:key", "6");
 ​
         //按规定范围取出数据 ,也可以取出单个数据 leftPop rightPop ,按这样的方式取出来,也就代表从列表中删除了。
         List<String> stringList = opsForList.range("left:key", 1, 10);
 ​
         stringList.forEach(System.out::print);
         //out::4321109876
     }
 ​
     /**
      * set 测试,set 数据结构是唯一字符串(成员)的无序集合
      * 想要详细了解,请查看:https://redis.io/docs/data-types/sets/
      * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】
      */
     @Test
     public void test5() {
         SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet();
 ​
         opsForSet.add("set:key","宁在春","1","1","2","3","开始学习");
 ​
         Set<String> members = opsForSet.members("set:key");
         members.forEach(System.out::print);
         // 3宁在春21开始学习
 ​
         System.out.println("");
         // 从 set 集合中删除 value 为 1 的值
         opsForSet.remove("set:key","1");
 ​
         Set<String> members2 = opsForSet.members("set:key");
         members2.forEach(System.out::print);
         //2宁在春3开始学习
         // 注意:取出来的时候,不一定是插入顺序
     }
 ​
     /**
      * Redis 哈希 测试,
      * Redis 哈希是结构为字段值对集合的记录类型。您可以使用散列来表示基本对象并存储计数器分组等。
      * 想要详细了解,请查看:https://redis.io/docs/data-types/hashes/
      * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】
      * 应用场景:可以存储登录用户的相关信息
      */
     @Test
     public void test6() {
         HashOperations<String, Object, Object> opsForHash = stringRedisTemplate.opsForHash();
         opsForHash.put("hash:key","username","宁在春");
         opsForHash.put("hash:key","school","xxxx学校");
         opsForHash.put("hash:key","age","3");
 ​
         Object username = opsForHash.get("hash:key", "username");
         System.out.println(username);
         //宁在春
         username="宁在春写的这篇文章还不错,值得一赞";
         // 更新某一个值数据
         opsForHash.put("hash:key","username",username);
         Object username2 = opsForHash.get("hash:key", "username");
         System.out.println(username2);
         //宁在春写的这篇文章还不错,值得一赞
 ​
         // 删除某一条数据
         opsForHash.delete("hash:key","age");
 ​
         // 展示某个key下所有的 hashKey
         Set<Object> keys = opsForHash.keys("hash:key");
         keys.forEach(System.out::println);
         //username
         //school
 ​
         // 展示某个key下所有 hashKey 对应 Value值
         List<Object> hashKeyValues = opsForHash.values("hash:key");
         hashKeyValues.forEach(System.out::println);
         //宁在春写的这篇文章还不错,值得一赞
         //xxxx学校
 ​
 ​
         HashMap<String, String> map = new HashMap<>();
         map.put("name","nzc");
         map.put("address","china");
         opsForHash.putAll("hash:key2",map);
 ​
         List<Object> values = opsForHash.values("hash:key2");
         values.forEach(System.out::println);
         //china
         //nzc
 ​
     }
 ​
     @Autowired
     private RedisTemplate<Object,Object> redisTemplate;
 ​
 ​
     /**
      *  上面的操作都是操作字符串,但是大家在使用的过程中都知道,我们大都是存放到Redis中的数据,都是将某个对象直接放入Redis中
      *
      *  redisTemplate 的操作和 stringRedisTemplate 的操作都是一样的,只是内部存放的不同罢了
      */
     @Test
     public void test7() {
         Map<String, Student> map = new HashMap<>();
         Student s1 = new Student();
         s1.setSchool("xxxx1");
         s1.setUsername("ningzaichun1号");
         s1.setAge(3);
         Student s2 = new Student();
         s2.setSchool("xxxx2");
         s2.setUsername("ningzaichun2号");
         s2.setAge(5);
         map.put("user:1",s1);
         map.put("user:2",s2);
         redisTemplate.opsForHash().putAll("student:key",map);
 ​
 ​
         List<Object> values = redisTemplate.opsForHash().values("student:key");
         values.forEach(System.out::println);
         //Student(username=ningzaichun1号, school=xxxx1, age=3)
         //Student(username=ningzaichun2号, school=xxxx2, age=5)
     }
 ​
     /**
      * 虽然大家可以在Java 程序中看到取出来的值是正常的,
      * 但是在平时开发和测试的时候,我们还是需要借助 Redis 的可视化工具来查看的,
      * 你会发现,我们采用默认的序列化机制(JDK序列化机制),在Redis可视化软件中,会无法直接查看的,
      * 都是转码之后的数据: \xac\xed\x00\x05t\x00\x06user:2
      * 图片见下面第二张图
      */
     @Test
     public void test8() {
         Map<String, Student> map = new HashMap<>();
         Student s1 = new Student();
         s1.setSchool("xxxx1");
         s1.setUsername("ningzaichun1号");
         s1.setAge(3);
         Student s2 = new Student();
         s2.setSchool("xxxx2");
         s2.setUsername("ningzaichun2号");
         s2.setAge(5);
         map.put("user:1",s1);
         map.put("user:2",s2);
         redisTemplate.opsForHash().putAll("student:key",map);
 ​
         List<Object> values = redisTemplate.opsForHash().values("student:key");
         values.forEach(System.out::println);
         //Student(username=ningzaichun1号, school=xxxx1, age=3)
         //Student(username=ningzaichun2号, school=xxxx2, age=5)
     }
 }
 ​

list数据类型演示 leftPushrightPush命令时的效果图:

image.png

使用 Redis 默认序列化机制,往Redis中存储对象的结果展示:

image.png

可以看到在可视化界面中,他们的值都被序列化成这么一串字符串了,不便于数据的查看。

将 Redis 的默认序列化机制,改成什么样的才合适呢?

改成 JSON 格式的序列化机制最佳,既方便查看,也支持各种各样的程序。

聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考2:https://developer.aliyun.com/article/1394670

目录
相关文章
|
2月前
|
运维 监控 关系型数据库
AI 时代的 MySQL 数据库运维解决方案
本文探讨了大模型与MySQL数据库运维结合所带来的变革,介绍了构建结构化运维知识库、选择合适的大模型、设计Prompt调用策略、开发MCP Server以及建立监控优化闭环等关键步骤。通过将自然语言处理能力与数据库运维相结合,实现了故障智能诊断、SQL自动优化等功能,显著提升了MySQL运维效率和准确性。
320 18
|
4月前
|
关系型数据库 数据库 RDS
【瑶池数据库训练营及解决方案本周精选(探索PolarDB,参与RDS迁移、连接训练营)】(5.30-6.8)
本周精选聚焦数据库迁移训练营、快速连接云数据库RDS训练营及智能多模态搜索解决方案。为用户提供模拟教程与实战演练,学习RDS MySQL实例连接与数据管理技能,助力企业智能化发展。每周解锁数据库实战新场景,抓紧时间,精彩不容错过!
|
4月前
|
Cloud Native 关系型数据库 分布式数据库
阿里云PolarDB与沃趣科技携手打造一体化数据库解决方案,助推国产数据库生态发展
阿里云瑶池数据库与沃趣科技将继续深化合作,共同推动国产数据库技术的持续创新与广泛应用,为行业生态的繁荣注入更强劲的技术动力。
阿里云PolarDB与沃趣科技携手打造一体化数据库解决方案,助推国产数据库生态发展
|
3月前
|
运维 监控 关系型数据库
AI 时代的 MySQL 数据库运维解决方案
本方案将大模型与MySQL运维深度融合,构建智能诊断、SQL优化与知识更新的自动化系统。通过知识库建设、大模型调用策略、MCP Server开发及监控闭环设计,全面提升数据库运维效率与准确性,实现从人工经验到智能决策的跃迁。
430 26
|
2月前
|
SQL 安全 关系型数据库
数据库安全管理新范式:DBKEEPER一体化数据库权限管控堡垒机解决方案
在数字化时代,数据库安全至关重要。DBKEEPER提供一站式数据库安全访问与权限管控解决方案,支持多种数据库,具备精细化权限管理、数据脱敏、高危操作拦截、全面审计等功能,助力企业实现智能、安全的数据治理,满足金融、医疗、互联网等行业合规需求。选择DBKEEPER,让数据库安全管理更高效!
数据库安全管理新范式:DBKEEPER一体化数据库权限管控堡垒机解决方案
|
6月前
|
关系型数据库 MySQL 数据库连接
docker拉取MySQL后数据库连接失败解决方案
通过以上方法,可以解决Docker中拉取MySQL镜像后数据库连接失败的常见问题。关键步骤包括确保容器正确启动、配置正确的环境变量、合理设置网络和权限,以及检查主机防火墙设置等。通过逐步排查,可以快速定位并解决连接问题,确保MySQL服务的正常使用。
937 82
|
5月前
|
负载均衡 算法 关系型数据库
大数据新视界--大数据大厂之MySQL数据库课程设计:MySQL集群架构负载均衡故障排除与解决方案
本文深入探讨 MySQL 集群架构负载均衡的常见故障及排除方法。涵盖请求分配不均、节点无法响应、负载均衡器故障等现象,介绍多种负载均衡算法及故障排除步骤,包括检查负载均衡器状态、调整算法、诊断修复节点故障等。还阐述了预防措施与确保系统稳定性的方法,如定期监控维护、备份恢复策略、团队协作与知识管理等。为确保 MySQL 数据库系统高可用性提供全面指导。
|
6月前
|
消息中间件 缓存 NoSQL
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
|
8月前
|
安全 网络安全 数据库
Access denied for user ‘qingtingstpublic’@’171.213.253.88’ (using password: YES)宝塔数据库远程无法连接-宝塔数据远程无法连接的正确解决方案-优雅草央千澈-问题解决
Access denied for user ‘qingtingstpublic’@’171.213.253.88’ (using password: YES)宝塔数据库远程无法连接-宝塔数据远程无法连接的正确解决方案-优雅草央千澈-问题解决
103 28
|
4月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?

热门文章

最新文章