首页 > 数据库 >基于redis的查询业务缓存实现

基于redis的查询业务缓存实现

时间:2022-10-16 22:36:21浏览次数:45  
标签:shop 缓存 return 数据库 redis 查询 id

添加缓存业务流程及代码实现

业务流程

说明

1、先从redis中进行查询,redis中如果有对应的数据则直接返回;如果没有再进入数据库查询

2、从数据库查询到的数据判断是否为空,非空写入redis再返回

代码实现

ShopServiceImpl.getByIdWithCache()

private StringRedisTemplate stringRedisTemplate;

public ShopServiceImpl(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
}

@Override
public Result getByIdWithCache(Long id) {
    String key = CACHE_SHOP_KEY + id;  //cache:shop:id

    //1、根据id在redis中进行查询
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    //2、如果查询到了,直接返回商铺的信息
    if(!StrUtil.isBlank(shopJson)){
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }

    //3、如果没有查到,在数据库中进行查找
    Shop shop = query().eq("id", id).one();

    //4、数据如果为空,直接返回
    if(shop == null){
        return Result.fail("无此店铺");
    }

    //5、数据不为空,将数据放至redis中,并设置过期时间
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);  //TTL过期时间

    return Result.ok(shop);
}

缓存更新策略

主要有三种:内存淘汰(redis内存不够用时剔除缓存)、超时剔除(等缓存过期更新)、主动更新(修改数据库时更新缓存,常用

业务场景:

低一致性需求:使用redis自带的内存淘汰即可。如店铺类型的查询

高一致性需求:主动更新,并以超时剔除作为兜底方案。如店铺详情的查询

主动更新策略

推荐采取Cache Aside Pattern,更新数据库,更新缓存

删除缓存还是更新缓存?

写多读少时,会存在大量用不到的缓存,如果每次写都修改缓存,可能造成缓存还未用到就已经失效了,造成了大量的无效写。因此在更新数据库时,将对应的redis缓存删除,再次查询时生成新的缓存。

如何保证缓存操作和数据库操作同时成功、同时失败?

在单体系统中,将二者的操作放在一个事务中;在分布式系统中,利用TCC等分布式事务方案

在保证操作原子性的基础上,应该先操作数据库还是先操作缓存?

应该先更新数据库中的数据,再删除缓存。考虑在并发情况下,如果先删除了redis的缓存,在完成写数据库之前有其他线程读取该条数据,则会重新生成旧的数据的redis缓存,并且由于数据库写数据的时间原大于redis删除数据的时间,造成此种情况的可能性非常大,数据不一致性的概率更大;而如果先更新数据库,那么redis删除数据的操作可以很快的接着数据库的写操作进行,造成数据不一致的概率小。

实现数据库和缓存的双写一致

ShopServiceImpl.updateByIdWithCache()

@Override
@Transactional
public Result updateByIdWithCache(Shop shop) {
    //先判断id是否为空
    Long id = shop.getId();
    if(id == null) return Result.fail("该商铺不存在");

    //1、更新数据库
    updateById(shop);

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

    return Result.ok("更新成功");
}

缓存穿透及解决方法

什么是缓存穿透?

当查询的数据不存在时,即redis和数据库上都查不到对应的数据时;造成的redis失效,使得所有的这些空数据请求全部打到数据库上,造成缓存穿透。

如何解决

主要的有两种:布隆过滤器、缓存空对象。

预防方案:增强id的复杂度避免猜测到id的规律、做好数据的基础格式校验、加强用户权限校验、做好热点参数的限流

缓存空对象解决缓存穿透

业务流程

说明

在基本的缓存业务上增加了两个逻辑判断

1、此时redis命中后需要判断是否为空

2、在数据库中如果查询的商铺数据为空,将空值写至redis中并返回

代码实现

对ShopServiceImpl.getByIdWithCache()进行优化,增加了redis空对象,防止缓存穿透

@Override
public Result getByIdWithCache(Long id) {
    String key = CACHE_SHOP_KEY + id;

    //1、根据id在redis中进行查询
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    //2、如果查询到了,直接返回商铺的信息
    if(!StrUtil.isBlank(shopJson)){
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        //为防止缓存穿透在redis中增加了空值对象,需要判断是否为空
        if(shop == null) return Result.fail("不存在该商铺");
        return Result.ok(shop);
    }

    //3、如果没有查到,在数据库中进行查找
    Shop shop = query().eq("id", id).one();

    //4、数据如果为空,在redis中记录空值并返回
    if(shop == null){
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "null", CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.fail("无此店铺");
    }

    //5、数据不为空,将数据放至redis中,并设置过期时间
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

    return Result.ok(shop);
}

缓存击穿及解决方法

什么是缓存击穿?

热点key突然失效了,在redis缓存中没有对应的数据,即没有命中缓存;而此时大量的请求都在请求这个数据,如果需要联表查询、或者获取数据比较慢时,无法及时返回数据,导致了大量的请求都越过redis缓存直接从数据库获取数据,无数请求访问给数据库带来巨大的冲击。

解决缓存击穿的两种思路

互斥锁解决缓存击穿:对商户的id加互斥锁,当有多个请求获取该商铺数据时,只允许互斥的访问数据库

逻辑过期解决缓存击穿:给商铺进行预热,提前将热点商铺信息放至redis中,设为永不过期。同时,为了保证数据的一致性,设置一个逻辑过期时间,在时间到了之后加锁并开辟一个新的线程进行更新;在更新数据时,未获取到锁的其他请求,获取redis中之前的旧数据返回,直至新的数据库数据更新至redis中

互斥锁解决缓存击穿

业务流程

代码实现

ShopServiceImpl.queryWithMutex()

/**
  * 在防止缓存穿透(缓存空对象)的基础上也实现了防止缓存击穿(互斥锁)
  * @param id
  * @return
  */
@Override
public Shop queryWithMutex(Long id) {

    String key = CACHE_SHOP_KEY + id;

    //1、根据id在redis中进行查询
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    //2、如果查询到了,直接返回商铺的信息
    if(StrUtil.isNotBlank(shopJson)){
        return JSONUtil.toBean(shopJson, Shop.class);
    }

    /**
     * ADD 如果未命中,则在此处进行缓存重建,在查询数据库前进行加锁
     */
    Shop shop = null;
    try {
        //TODO ADD1尝试获取互斥锁
        boolean hasLock = tryLock(LOCK_SHOP_KEY + id);
        //TODO 获取锁之后应该再查一遍redis中是否有数据,可能其他请求在使用锁时就已经将数据存到redis中了

        //TODO ADD2判断是否获取成功
        if(!hasLock){
            Thread.sleep(50);
            //TODO ADD3如果失败休眠后重试
            return queryWithMutex(id);  //此处用递归就是有一个刷新redis的效果,再次获取锁之前去redis中再查一遍
        }

        //TODO ADD4获取锁成功,进入数据库查询
        //3、如果没有查到,在数据库中进行查找
        shop = query().eq("id", id).one();
        Thread.sleep(500);  //模拟缓存重建时间很长的情况下

        //4、数据如果为空,将空对象放至redis中
        if(shop == null){
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "null", CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return null;
        }

        //5、数据不为空,将数据放至redis中,并设置过期时间
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

    }catch (InterruptedException e) {
        throw new RuntimeException();
    }finally {
        //TODO ADD5释放互斥锁
        unLock(LOCK_SHOP_KEY + id);
    }

    return shop;
}

//利用redis加临时互斥锁
private boolean tryLock(String key){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);  //10s后过期
    return BooleanUtil.isTrue(flag);
}

private void unLock(String key){
    stringRedisTemplate.delete(key);
}

逻辑过期解决缓存击穿

业务流程

代码实现

RedisData.java

/**
 * 基于逻辑过期解决缓存击穿
 * 将过期时间和实际对象进行封装
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

ShopServiceImpl.queryWithLogicalExpire()

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

public Shop queryWithLogicalExpire(Long id){
    String key = CACHE_SHOP_KEY + id;

    //1、根据id在redis中进行查询
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    //2、如果未命中,直接返回空值
    if(StrUtil.isBlank(shopJson)){
        return null;
    }

    //3、命中,需要把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject data = (JSONObject) redisData.getData();
    LocalDateTime expireTime = redisData.getExpireTime();
    Shop shop = JSONUtil.toBean(data, Shop.class);

    //4、判断逻辑逻辑时间是否过期
    if(LocalDateTime.now().isAfter(expireTime)){
        //5、未过期,直接返回商铺信息
        return shop;
    }

    String lockKey = LOCK_SHOP_KEY + id;

    //6、过期,尝试获取互斥锁
    boolean isLock = tryLock(lockKey);

    //7、如获取到互斥锁,开启一个新的线程,进行缓存重建
    //获取到锁后应进行DoubleCheck,再次查询Redis
    if(isLock){
        //重新查数据库并将新数据写到redis中
        CACHE_REBUILD_EXECUTOR.submit(()->{
            try {
                //8、缓存重建
                saveShop2Redis(id, 20L);
            }catch (Exception e){
                throw new RuntimeException();
            }finally {
                //8、释放锁
                unLock(lockKey);
            }

        });
    }

    //9、返回redis中的旧数据
    return shop;
}

@Override  //提前做数据预热,将热点店铺信息先存入redis中
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
    //1、查询店铺数据
    Shop shop = query().eq("id", id).one();
    Thread.sleep(200);  //模拟数据库联表查询的耗时
    //2、封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //3、写入Redis,不设过期时间
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

缓存雪崩及解决方法

什么是缓存雪崩?

同一时间缓存中大量的key同时失效,或者redis服务宕机,导致大量请求到达数据库,带来巨大压力

解决方法

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

标签:shop,缓存,return,数据库,redis,查询,id
From: https://www.cnblogs.com/Gw-CodingWorld/p/16797450.html

相关文章

  • 数据库学习笔记04- redis
    5,Redis基础redis--KV数据库--内存--单线程+异步i/o(多路io复用)计算密集型应用:多进程+多进程IO密集型应用:单线程+异步IO(协程)2008年--redis--》REmote......
  • 09.多表查询
    多表查询一、笛卡尔乘积--笛卡尔乘积--查询结果将People所有记录和Department所有记录依次排列组合形成新的结果select*fromPeople,Department;二、简单的多表查询......
  • 06.模糊查询
    模糊查询模糊查询使用like关键字和通配符结合来实现,通配符具体含义如下:%:代表匹配0个字符、1个字符或多个字符_:代表匹配有且只有1个字符[]:代表匹配范围内[^]......
  • 08.分组查询
    分组查询--根据员工所在地区分组,统计员工人数,工资总和,平均工资,最高工资,最低工资--方案1select'武汉'地区,count(*)员工人数,sum(PeopleSalary)工资总和,avg(People......
  • Redis学习笔记
    基础篇-02.初识Redis-认识NoSQL_哔哩哔哩_bilibili,参考黑马程序员出品的Redis教程,感谢黑马!基础篇一、Redis入门1.认识NoSQL1.1 什么是NoSQLNoSQL最常见的解释是"n......
  • 04.基本查询
    基本查询--查询所有列所有行--*代表查询所有列,未加限制条件说明查找所有行select*fromDepartmentselect*from[Rank]select*fromPeople--查询员工表中(姓名,性别......
  • 05.条件查询
    条件查询SQL中常用的运算符=等于,比较是否相等及赋值!=比较不等于>比较大于<比较小于>=比较大于等于<=比较小于等于ISNULL比较为......
  • Centos7部署redis三节点哨兵集群,添加布隆过滤器
    目录Centos7部署redis三节点哨兵集群,添加布隆过滤器一、环境准备1.1、服务器准备1.2、依赖安装二、部署redis2.1、安装redis2.2、修改配置文件2.3、加入systemctl管理三、......
  • Redis数据结构之字符串
    目录Redis数据结构之字符串添加获取修改删除判断一个key是否存在查看过期时间设置过期时间合并set和ex合并set和px判断一个key是否存在,存在则忽略,不存在则创建合并set和nx......
  • Redis数据结构之列表
    目录Redis数据结构之列表查看命令帮助创建列表从左边插入元素从右边插入数据若list存在,则从左边依次追加元素,不存在则忽略若list存在,则从右边依次追加元素,不存在则忽略从li......