前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >③使用Redis缓存,并增强数据一致性。

③使用Redis缓存,并增强数据一致性。

作者头像
.29.
发布2024-01-09 13:57:47
1300
发布2024-01-09 13:57:47
举报
文章被收录于专栏:个人技术博客个人技术博客

个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客

在这里插入图片描述
在这里插入图片描述
使用Redis缓存,并增强数据一致性。
  • Redis缓存
    • 🚀为什么使用缓存?
    • 🚀如何添加Redis缓存?
    • 🚀缓存数据一致性问题(双写问题)
    • 🚀实现 缓存与数据库双写一致(此方式不能保证绝对一致)

Redis缓存

🚀为什么使用缓存?

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力。

  • 缓存的作用:
    • 降低后端负载。
    • 提高读写效率,降低响应时间。

使用缓存的同时,也会增加代码复杂度和运营的成本。

  • 缓存的成本:
    • 数据一致性成本(双写问题)
    • 代码维护成本
    • 运维成本

缓存的使用案例:

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码(例如:

代码语言:javascript
复制
// 例1:本地用于高并发
Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 

//例2:用于redis等缓存
static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 

//例3:本地缓存
Static final Map<K,V> map =  new HashMap(); 

由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;


🚀如何添加Redis缓存?

Redis缓存作用模型

标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis。

在这里插入图片描述
在这里插入图片描述

为查询的数据添加缓存 业务逻辑

代码语言:javascript
复制
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 根据id查询商铺信息
    @Override
    public Result queryById(Long id) {
        // redis缓存的key
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //1. 从redis缓存中获取shop信息
        String shopJSON = stringRedisTemplate.opsForValue().get(key);

        //2. 缓存存在,返回(Hutool工具:StrUtil、JSONUtil)
        if(StrUtil.isNotBlank(shopJSON)){
            Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
            return Result.ok(shop);
        }

        //3. 缓存未命中,从数据库中获取
        Shop shop = this.getById(id);

        //4. 数据库中不存在,返回错误
        if(shop == null) return Result.fail("店铺不存在!");

        //5. 数据库中存在,存入redis缓存(Hutool工具:JSONUtil)
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));

        //6. 返回
        return Result.ok(shop);
    }

🚀缓存数据一致性问题(双写问题)

双写问题

双写问题通常出现在以下场景:

  1. 写入数据源: 应用程序接收到写入请求后,首先将数据写入主要的数据源(例如数据库)。
  2. 写入缓存: 同时,应用程序尝试将相同的数据写入缓存,以提高后续对该数据的读取性能。

在这个过程中,如果写入数据源成功而写入缓存失败,或者写入缓存成功而写入数据源失败,就会导致数据不一致的情况。例如:

  • 写入数据源成功,写入缓存失败: 在这种情况下,缓存中可能没有最新的数据,而应用程序仍然从缓存中读取旧数据,导致不一致。
  • 写入缓存成功,写入数据源失败: 这种情况下,缓存中包含了最新的数据,但是由于数据源没有更新,当应用程序从数据源中读取数据时,可能得到旧的数据,同样导致不一致。

解决方案

  • Cache Aside Pattern人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
  • Read/Write Through Pattern : 缓存与数据库整合为一个服务,用服务来维护一致性。调用者调用该服务,无需关系缓存一致性问题。即:由系统本身完成,数据库与缓存的问题交由系统本身去处理
  • Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

使用Cache Aside Pattern人工编码方式,需要注意的问题

  • 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(×)
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(
  • 如何保证缓存与数据库操作同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  • 先操作缓存还是先操作数据库?
    • 选择①:先删除缓存,再操作数据库
    • 选择②:先操作数据库,再删除缓存(
    • 应该具体操作缓存还是操作数据库? 我们应当是先操作数据库,再删除缓存 ,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

🚀实现 缓存与数据库双写一致(此方式不能保证绝对一致)

流程

  • 查询数据时,若缓存未命中,从数据库中获取,再将结果写入缓存,设置过期时间(TTL)。
  • 修改数据时,先更新数据库,再删除缓存。

查询数据时

代码语言:javascript
复制
    // 根据id查询商铺信息
    @Override
    public Result queryById(Long id) {
        // redis缓存的key
        String key = "cache:shop:" + id;
        //1. 从redis缓存中获取shop信息
        String shopJSON = stringRedisTemplate.opsForValue().get(key);

        //2. 缓存存在,返回
        if(StrUtil.isNotBlank(shopJSON)){
            Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
            return Result.ok(shop);
        }

        //3. 缓存未命中,从数据库中获取
        Shop shop = this.getById(id);

        //4. 数据库中不存在,返回错误
        if(shop == null) return Result.fail("店铺不存在!");

        //5. 数据库中存在,存入redis缓存,并设置过期时间ttl
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);

        //6. 返回
        return Result.ok(shop);
    }

查询数据时(解决缓存穿透):

代码语言:javascript
复制
    // 根据id查询商铺信息(缓存空值,避免缓存穿透问题)
    @Override
    public Result queryById(Long id) {
        // redis缓存的key
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //1. 从redis缓存中获取shop信息
        String shopJSON = stringRedisTemplate.opsForValue().get(key);

        if(shopJSON == null){ // 获取值为空,返回错误
            return Result.fail("商铺不存在!");
        }

        //2. 缓存存在,返回
        if(StrUtil.isNotBlank(shopJSON)){
            Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
            return Result.ok(shop);
        }

        //3. 缓存未命中,从数据库中获取
        Shop shop = this.getById(id);

        //4. 数据库中不存在,空值写入Redis,返回错误
        if(shop == null){
            // 控制写入Redis,设置2分钟有效期
            stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
            //返回错误
            return Result.fail("商铺不存在!");
        }

        //5. 数据库中存在,存入redis缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);

        //6. 返回
        return Result.ok(shop);
    }

修改数据时

代码语言:javascript
复制
    @Override
    @Transactional  //开启事务,保证证缓存与数据库操作同时成功或失败
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null) return Result.fail("商铺ID不能为空!");
        
        //注意: 先更新数据库再删除缓存
        
        //1. 更新数据库
        this.updateById(shop);

        //2. 删除缓存
        stringRedisTemplate.delete("cache:shop:" + id);

        return Result.ok();
    }

在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用Redis缓存,并增强数据一致性。
  • Redis缓存
    • 🚀为什么使用缓存?
      • 🚀如何添加Redis缓存?
        • 🚀缓存数据一致性问题(双写问题)
          • 🚀实现 缓存与数据库双写一致(此方式不能保证绝对一致)
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档