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

一、缓存

1、缓存的基本知识

1)什么是缓存?

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

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

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

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

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

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

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

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

4)缓存的使用流程

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

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;
     }
 ​
 }
复制代码

问题:

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


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

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

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

二、集成 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的自动配置类

在这里可以看到它给我们自动注入了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 中也有很多其他的数据结构
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值