Redis学习之缓存实现及缓存更新

介绍

什么是缓存?

缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。

image-20230920162703243

为什么需要缓存?

提前准备好数据,便于更快地读写。

缓存是把双刃剑,要权衡利弊。

优点:降低后端负载

提高读写效率,降低响应时间

缺点:数据一致性成本 代码维护成本 运维成本

实现

关键流程

1.暂无缓存,从数据库读,然后设置(更新)缓存

2.已有缓存,直接读缓存

image-20230920163238784

代码示例

ShopController

    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id)    {
        return shopService.queryById(id);
    }

ShopServiceImpl

    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return Result.fail("店铺不存在!");
        }
        // 4.不存在,根据id查询数据库
        Shop shop = getById(id);
        if (shop == null){
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 5.不存在返回错误
            return Result.fail("店铺不存在!");
        }
        // 6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return Result.ok(shop);
    }

缓存更新

几种常用策略,一般会选择主动更新+超时提出兜底。

image-20230920163649109

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:

用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等

主动更新缓存的几种方式

1.Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。(以数据库的数据为准,业务层自己来控制缓存的写入)

2.Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理.(使用现成的数据写入服务,让服务来维护数据库和缓存的一致性,我门只用写数据即可)

3.Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致。(先更新缓存,通过异步线程定期将缓存的数据持久化(同步道数据库中))

操作缓存和数据库时有三个问题需要考虑: 1.删除缓存还是更新缓存? 更新缓存:每次更新数据库都更新缓存,无效写操作较多 删除缓存:更新数据库时让缓存失效,查询时再更新缓存

2.多线程情况下,如何保证缓存与数据库的操作的同时成功或失败? 1.单体系统:将缓存与数据库操作放在一个事务 2.分布式系统:利用TCC等分布式事务方案

3.先操作缓存还是先操作数据库?

下图为两种方式在多线程下可能出现的问题

image-20230920164323279

两种方式都不能百分百保证一致性,但【先操作数据库,再删除缓存】,出现问题的几率低,可以配合延时双删保证缓存一定被删除。

实现

核心思路如下:

修改ShopController中的业务逻辑,满足下面的需求:

根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

根据id修改店铺时,先修改数据库,再删除缓存

修改重点代码1:修改ShopServiceImpl的queryById方法

设置redis缓存时添加过期时间

image-20230920164604030

修改重点代码2

代码分析:通过之前的淘汰,我们确定了采用删除策略,来解决双写问题,当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从mysql中加载最新的数据,从而避免数据库和缓存不一致的问题

    @Transactional
    @Override
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        // 1.更新数据库
            updateById(shop);
        // 2.输出缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

问题及解决

缓存穿透

攻击者可以恶意请求数据库中不存在的数据,从而使得每次查询都要绕过缓存查数据库,增大数据库的压力。

解决方案:

1.缓存空值:比如塞一个空字符串。注意可以给空对象的键过期时间设置短一些,或者在新增数据时强制清除下对应缓存(防止查出来还是 null)

2.布隆过滤器

image-20230920165041595

预防做法:

1.增强对请求数据的校验,比如 id > 0

2.增强对数据格式的控制,比如 id 设置为 10 位,不为 10 位的请求直接拒绝

3.增强用户权限校验

4.通过限流来保护数据库

修改后的代码

@Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return Result.fail("店铺不存在!");
        }
        // 4.不存在,根据id查询数据库
        Shop shop = getById(id);
        if (shop == null){
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 5.不存在返回错误
            return Result.fail("店铺不存在!");
        }
        // 6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return Result.ok(shop);
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值