redisredis的缓存使用技巧(商户查询缓存)(代码片段)

吞吞吐吐大魔王 吞吞吐吐大魔王     2022-10-22     520

关键词:

文章目录

1. 什么是缓存

缓存(Cache) 就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

2. 添加 Redis 缓存

2.1 缓存工作模型

2.2 代码实现

前端请求说明:

说明
请求方式POST
请求路径/shop/id
请求参数id
返回值

后端接口实现:

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService 

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryShopById(Long id) 
        String key = "cache:shop:" + id;
        // 1. 从 redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if(!StrUtil.isBlank(shopJson)) 
            // 3. 存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        
        // 4. 不存在,根据 id 查询数据库
        Shop shop = getById(id);
        // 5. 不存在,返回错误
        if(shop == null)
            return Result.fail("店铺不存在!");
        
        // 6. 存在,写入 redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        // 7. 返回
        return Result.ok(shop);
    

3. 缓存更新策略

3.1 缓存更新策略类型

缓存更新策略内存淘汰超时剔除主动更新
说明不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存。给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。编写业务逻辑,在修改数据库的同时,更新缓存。
一致性一般
维护成本

业务场景:

  • 低一致性:使用内存淘汰机制。
  • 高一致性:主动更新,并以超时剔除作为兜底方案。

3.2 主动更新策略

方式描述
Cache Aside Pattern由缓存的调用者,在更新数据库的同时更新缓存。
Read/Write Through Pattern缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
Write Behind Caching Pattern调用者只操作缓存,由其它线程异步的将缓存持久化到数据库,保证最终一致性。

这里推荐使用 Cache Aside Pattern,但操作缓存和数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?

    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多。
    • 删除缓存(推荐):更新数据库时让缓存失效,查询时再更新缓存。
  2. 如何保证缓存与数据库的操作的同时成功或失败?

    • 单体系统:将缓存与数据库操作放在一个事务。
    • 分布式系统:利用 TTC 等分布式事务方案。
  3. 先操作缓存还是先操作数据库?

    • 先删除缓存,再操作数据库。(问题:当一个线程进行修改操作时,先删除了缓存,然后另一个线程读取,读取不到缓存便读取数据库,然后更新缓存,更新的是旧的数据库的值,最后第一个线程又更新数据库,导致数据库和缓存不一致。这种问题出现的概率比较高。
    • 先操作数据库,再删除缓存。(推荐。问题:当一个线程读取时正好缓存过期,那么将读取到数据库的数据,然后另一个线程进入进行修改操作,修改数据库后,将缓存删除。最后第一个线程将之前读取的数据写入缓存,就会造成数据库和缓存不一致。但读取缓存是微秒级的并又正好碰上缓存过期,因此该问题的概率很小。)

小结:

  • 读操作:缓存命中直接返回;缓存未命中则查询数据库,并写入缓存,设定超时时间。
  • 写操作:先写数据库,然后再删缓存。要确保数据库与缓存操作的原子性。

3.3 超时剔除和主动更新缓存实现

后端接口实现:

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

    @Service
    public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService 
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result queryShopById(Long id) 
            String key = "cache:shop:" + id;
            // 1. 从 redis 查询商铺缓存
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            // 2. 判断是否存在
            if(!StrUtil.isBlank(shopJson)) 
                // 3. 存在,直接返回
                Shop shop = JSONUtil.toBean(shopJson, Shop.class);
                return Result.ok(shop);
            
            // 4. 不存在,根据 id 查询数据库
            Shop shop = getById(id);
            // 5. 不存在,返回错误
            if(shop == null)
                return Result.fail("店铺不存在!");
            
            // 6. 存在,写入 redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
            stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
            // 7. 返回
            return Result.ok(shop);
        
    
    
  • 通过 id 修改店铺时,先修改数据库,再删除缓存。

    @Service
    public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService 
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        
    	@Override
        @Transactional
        public Result updateShop(Shop shop) 
            Long id = shop.getId();
            if(id == null)
                return Result.fail("店铺 id 不能为空!");
            
            // 1. 更新数据库
            updateById(shop);
            // 2. 删除缓存
            stringRedisTemplate.delete("cache:shop:" + id);
            return Result.ok();
        
    
    

4. 缓存穿透

4.1 基本介绍

缓存穿透 是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不生效,这些请求都会打到数据库。(如果不断发起这样的请求,会给数据库带来巨大压力)

常见解决方案:

方案描述优点缺点
缓存空对象如果请求的数据缓存不存在,并且数据库也不存在,数据库将给缓存更新个空对象。实现简单,维护方便。额外的内存消耗,可能造成短期的不一致。
布隆过滤器内存占用较少,没有多余 key实现复杂,存在误判可能
增强 id 的复杂度,避免被猜测 id 规律
做好数据的基础格式校验
做好热点参数的限流

4.2 通过缓存空对象解决缓存穿透问题

代码实现:

public Shop queryWithPassThrough(Long id) 
    String key = "cache:shop:" + id;
    // 1. 从 redis 查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (!StrUtil.isBlank(shopJson)) 
        // 3. 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    
    // 判断命中的是否为空值
    if (shopJson != null) 
        return null;
    
    // 4. 不存在,根据 id 查询数据库
    Shop shop = getById(id);
    // 5. 不存在,返回错误
    if (shop == null) 
        // 将空值写入 Redis
        stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    
    // 6. 存在,写入 redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
    stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
    // 7. 返回
    return shop;

5. 缓存雪崩

缓存雪崩 是指在同一时段大量的缓存 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的 key 的 TTL 添加随机值
  • 利用 Redis 集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

6. 缓存击穿

6.1 基本介绍

缓存击穿 问题也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见解决方案:

解决方案优点缺点
互斥锁没有额外的内存消耗;保证一致性;实现简单线程需要等待,性能受影响;可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性;有额外内存消耗;实现复杂

6.2 基于互斥锁方式解决缓存击穿问题

这里通过 Redis 中的 SETNX 命令去自定义一个互斥锁,通过 del 命令去删除这个 key 来解锁。

自定义尝试获取锁和释放锁实现

// 尝试获取锁
private boolean tryLock(String key)
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 拆箱过程可能有空值
    return BooleanUtil.isTrue(flag);


// 释放锁
private void unlock(String key)
    stringRedisTemplate.delete(key);

业务逻辑实现:

@Override
public Result queryShopById(Long id) 
    // 互斥锁缓存击穿
    Shop shop = queryWithMutex(id);
    if(shop == null)
        Result.fail("店铺不存在!");
    
    // 7. 返回
    return Result.ok(shop);


// 互斥锁存击穿
public Shop queryWithMutex(Long id)
    String key = "cache:shop:" + id;
    // 1. 从 redis 查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if(!StrUtil.isBlank(shopJson)) 
        // 3. 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    
    // 判断命中的是否为空值
    if(shopJson != null )
        return null;
    

    // 4. 实现缓存重建
    // 4.1 获取互斥锁
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try 
        boolean isLock = tryLock(lockKey);
        // 4.2 判断是否获取成功
        if(!isLock) 
            // 4.3 失败,则休眠并重试
            Thread.sleep(50);
            return queryWithMutex(id);
        
        // 4.4 成功,则根据 id 查询数据库
        shop = getById(id);
        // 5. 不存在,返回错误
        if(shop == null)
            // 将空值写入 Redis
            stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        
        // 6. 存在,写入 redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
        // 7. 释放互斥锁
        unlock(lockKey);
     catch (InterruptedException e) 
        throw new RuntimeException(e);
    
    // 8. 返回
    return shop;

6.3 基于逻辑过期方式解决缓存击穿问题

在不修改原有实体类的情况下,可以新定义一个类用来保存原有的数据并添加逻辑过期时间

@Data
public class RedisData 
    // 逻辑过期时间
    private LocalDateTime expireTime;
    // 要存储到 Redis 中的数据
    private Object data;

将店铺数据和逻辑过期时间封装并保存到 Redis 中

public void saveShop2Redis(Long id, Long expireSeconds)
    // 1. 查询店铺数据
    Shop shop = getById(id);
    // 2. 封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    // 3. 写入 Redis
    stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisData));

业务实现:

// 定义线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

// 基于逻辑过期缓存穿透
public Shop queryWithLogicalExpire(Long id) 
    String key = "cache:shop:" + id;
    // 1. 从 redis 查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (StrUtil.isBlank(shopJson)) 
        // 3. 不存在,直接返回
        return null;
    
    // 4. 命中,需要吧 json 反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 因为 data 类型为 Object,并不知道为 Shop,这里会转成 JSONObject
    JSONObject data = (JSONObject) redisData.getData();
    Shop shop = JSONUtil.toBean(data, Shop.class);
    // 5. 判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) 
        // 5.1 未过期,直接返回店铺信息
        return shop;
    
    // 5.2 已过期,需要缓存重建
    // 6. 缓存重建
    // 6.1 获取互斥锁
    String lockKey = "lock:shop:" + id;
    boolean isLock = tryLock(lockKey);
    // 6.2 判断是否获取锁成功
    if (isLock) 
        // 6.3 成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> 
            // 重建缓存
            this.saveShop2Redis(id, 1800L);
            // 释放锁
            unlock(lockKey);
        );
    
    // 6.4 返回过期的店铺信息
    return shop;

7. 缓存工具封装

接下来将对以下四个方法进行封装:

  1. 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置 TTL 过期时间

  2. 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置逻辑过期时间,用于处理缓存击穿问题

  3. 根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

  4. 根据指定的 key 查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿问题

@Slf4j
@Component
public class CacheClient 

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) 
        this.stringRedisTemplate = stringRedisTemplate;
    

    // 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置 TTL 过期时间
    public void set(String key, Object value, Long time, TimeUnit unit) 
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    

    // 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置逻辑过期时间,用于处理缓存击穿问题
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) 
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入 redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    

    // 根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) 
        String key = keyPrefix + id;
        // 1. 从 redis 查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (!StrUtil.isBlank(json)) 
            // 3. 存在,直接返回
            return

redisredis缓存穿透和雪崩

👉视频:【狂神说Java】Redis最新超详细版教程通俗易懂👇学习笔记Redis学习结束,继续消化补充~~~~Redis缓存穿透和雪崩(面试高频,工作常用)服务的高可用问题Redis缓存的使用,极大的提升了应... 查看详情

什么是/使用缓存(cache),缓存更新策略数据库缓存不一致解决方案及实现缓存与数据库双写一致(代码片段)

(目录)实现这个方案:商户查询缓存商户查询缓存1.什么是缓存(Cache)?前言:什么是缓存?举个例子:例如:例1:StaticfinalConcurrentHashMap<K,V>map=newConcurrentHashMap<>();例2:staticfinalCache<K,V>USER_CACHE=CacheBuilder.newBuilder().b 查看详情

redisredis缓存穿透

目录一、什么是缓存穿透二、解决缓存穿透2.1缓存空对象2.2拉黑ip2.3参数合法性校验2.4布隆过滤器一、什么是缓存穿透1.客户端请求的数据在数据库中没有,这样缓存永远不会生效,所有客户端请求都会访问数据库二、解... 查看详情

redisredis知识点总结(代码片段)

redisredis支持事务、持久化、LUA脚本、LRU驱动时间、多体集群redis与本地缓存(map)的区别?本地缓存:主要特点轻量以及快速,生命周期随着JVM的销毁而结束,多实例各自保存一份缓存,不具有一致性r... 查看详情

redisredis-cli操作指令

默认选择db库是0redis-cli-p6379 查看当前所在“db库”所有的缓存keyredis127.0.0.1:6379> keys* 选择db库redis127.0.0.1:6379>select8 清除所有的缓存keyredis127.0.0.1:6379> FLUSHALL 清除当前“db库”所有的缓存key 查看详情

hibernate缓存何时使用和如何使用

参考技术A   关于hibernate缓存的问题  基本的缓存原理  Hibernate缓存分为二级  第一级存放于session中称为一级缓存默认带有且不能卸载  第二级是由sessionFactory控制的进程级缓存是全局共享的缓存凡是会调用... 查看详情

查询缓存amazon redshift

】查询缓存amazonredshift【英文标题】:Querycacheamazonredshift【发布时间】:2018-11-2506:13:42【问题描述】:我在几天的间隔内运行了相同的查询,但在AmazonRedshift上的第一次执行运行时执行时间不同。对于两次运行,我都将会话的缓... 查看详情

redis总结

目录什么是RedisRedis为什么快?Redis有哪些常用的数据类型?RedisRDB和AOF持久化的区别,如何选择?什么是Redis持久化?Redis的持久化机制是什么?RDB和AOF的优缺点如何选择如何解决缓存击穿、缓存穿透、雪崩问题?缓存击穿... 查看详情

13.缓存(代码片段)

13.缓存缓存简介什么是缓存(cache)?存储在内存中的临时数据将用户经常查询的数据放在内存(缓存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,提高了查询效率,解决了高并发系统的性能问... 查看详情

使用查询参数缓存获取的破坏图像

】使用查询参数缓存获取的破坏图像【英文标题】:Cachebustingimagesthatarefetchedusequeryparams【发布时间】:2019-11-1517:08:51【问题描述】:我正在使用以下查询参数从url源获取图像:www.server.com/get?img.jpg但是当服务器更改图像时,由于... 查看详情

如何在不使用查询缓存的情况下缓存 Spring Data JPA 查询方法的结果?

】如何在不使用查询缓存的情况下缓存SpringDataJPA查询方法的结果?【英文标题】:HowtocacheresultsofaSpringDataJPAquerymethodwithoutusingquerycache?【发布时间】:2014-12-0206:18:37【问题描述】:我有一个带有SpringDataJPA(休眠后端)存储库类的... 查看详情

12.查询缓存

查询缓存的使用,主要是为了提高查询访问速度。将用户对同一数据的重复查询过程简化,不再每次均从数据库查询获取结果数据,从而提高访问速度。MyBatis的查询缓存机制,根据缓存区的作用域(生命周期)可划分为两种:... 查看详情

mybatis:缓存(代码片段)

目录缓存介绍MyBatis缓存一级缓存测试一级缓存失效的四种情况二级缓存使用步骤缓存原理整合第三方缓存EHCache缓存介绍什么是缓存[Cache]?存在内存中的临时数据。将用户经常查询的数据放在缓存(内存)中,用户去查询数据就... 查看详情

mybatis查询结果的缓存

MyBatis的缓存指的是缓存查询结果,当以后使用相同的sql语句、传入相同的参数进行查询时,可直接从mybatis本地缓存中获取查询结果,而不必查询数据库。mybatis的缓存包括一级缓存、二级缓存,一级缓存默认是开启的,二级缓存... 查看详情

如何缓存一个经常使用的查询? [关闭]

】如何缓存一个经常使用的查询?[关闭]【英文标题】:Howtocacheaquerywhichisoftenused?[closed]【发布时间】:2012-10-1115:36:17【问题描述】:当网页应用会频繁查询某些信息时,如何通过缓存查询结果来提高性能?(信息就像网站上的... 查看详情

使用ef查询有缓存的问题

...,而数据库的数据更新了,找了一点资料,是因为ef6有个缓存机制;Repository类://此方法查询结果有缓存publicList<T>FindAll(){returncontext.Set<T>().ToList();}///去掉次缓存publicList<T>FindAll(){returncont 查看详情

数据库缓存

...据库和服务器都是一种巨大的压力,为了解决此类问题,缓存数据的概念应运而生。b) 极大地解决数据库服务器的压力c) 提高应用数据的响应速度d) 常见的缓存形式:内存缓存(可避免I/O开销)、文件缓存2.Whya) ... 查看详情

不使用 Grails 查询缓存

】不使用Grails查询缓存【英文标题】:Grailsquerycacheisnotused【发布时间】:2013-05-1316:34:18【问题描述】:我正在尝试缓存从控制器调用的以下查询:defapprovedCount=Book.countByApproved(true,[cache:true])我已通过添加为Book类启用了二级缓存st... 查看详情