Redis在数据中进行缓存更新,以根据id查询商品为例

本文介绍了在SpringBoot应用中如何通过添加商户缓存来提高查询性能,讨论了缓存击穿问题以及两种解决方案:互斥锁和逻辑过期。作者详细描述了如何使用锁机制避免并发问题,以及如何通过封装Redis工具类实现这两种策略的通用化处理。
摘要由CSDN通过智能技术生成

商户查询缓存

在这里插入图片描述

1.1 添加商户缓存

在我们查询商户信息时,我们是直接操作从数据库中去进行查询的,大致逻辑是这样,直接查询数据库那肯定慢咯,所以我们需要增加缓存

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    //这里是直接查询数据库
    return shopService.queryById(id);
}

代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入redis。

@Override
public Result queryById(Long id) {
    //1.先到redis里面获取数据看是否能获取到。
    //1.1生成key
    String key = RedisConstants.CACHE_SHOP_KEY+id;
    //1.2 根据key获取shop对象
    Shop shop = (Shop)redisTemplate.opsForValue().get(key);
    if(ObjectUtil.isNotEmpty(shop)){
        //2.如果拿到了,直接返回数据。
        return Result.ok(shop);
    }
    //3.如果没有拿到,调用数据库获取数据。
    shop = getById(id);
    if(ObjectUtil.isEmpty(shop)){
        return Result.fail("没有该商户信息。。。");
    }
    //4.再把数据库中拿到的数据放入到redis里面。
    redisTemplate.opsForValue().set(key,shop,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //5.返回数据给控制器
    return Result.ok(shop);
}

1.2 缓存击穿问题及解决思路

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

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期
    在这里插入图片描述

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

解决方案一、使用锁来解决:

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

在这里插入图片描述

解决方案二、逻辑过期方案

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

互斥锁方案: 由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响 由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响

逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

1.3 利用互斥锁解决缓存击穿问题

我们可以使用synchronized解决这个问题。

@Override
public Result queryById(Long id) {
    //1.先到redis里面获取数据看是否能获取到。
    //1.1生成key
    String key = RedisConstants.CACHE_SHOP_KEY+id;
    //1.2 根据key获取shop对象
    Shop shop = (Shop)redisTemplate.opsForValue().get(key);
    if(ObjectUtil.isNotEmpty(shop)){
        //2.如果拿到了,直接返回数据。
        return Result.ok(shop);
    }
    //谁获取到锁谁到数据库里面取数据。
    synchronized (this) {
        //到redis里面获取商户信息
        shop = (Shop)redisTemplate.opsForValue().get(key);
        if(ObjectUtil.isNotEmpty(shop)){
            //2.如果拿到了,直接返回数据。
            return Result.ok(shop);
        }
        //3.如果没有拿到,调用数据库获取数据。
        shop = getById(id);
        if (ObjectUtil.isEmpty(shop)) {
            return Result.fail("没有该商户信息。。。");
        }
        //4.再把数据库中拿到的数据放入到redis里面。
        redisTemplate.opsForValue().set(key, shop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    }
    //5.返回数据给控制器
    return Result.ok(shop);
}

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿

1.4 利用逻辑过期解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

如果封装数据:因为现在redis中存储的数据的value需要带上过期时间,此时要么你去修改原来的实体类,要么你

步骤一、

新建一个实体类。

@Data
@NoArgsConstructor
@AllArgsConstructor
//逻辑过期的判断
public class RedisData {
    //逻辑过期时间
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime expireTime;
    //商户对象
    private Object data;
}

步骤二、

因为逻辑过期商户的信息永不过期,所以我们在添加商户的时候可以把商户信息放入到redis里面进行缓存。目前我们的项目没有后台管理系统,我们就写一个测试方法把商户信息缓存到redis里面。

@Test
public void testRedis(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    RedisTemplate redisTemplate = (RedisTemplate) ac.getBean("redisTemplate");
    IShopService shopService = (IShopService) ac.getBean("shopServiceImpl");
    //把id为1的商户信息保证到redisData里面放入到redis里面。
    Shop shop = shopService.getById(1);
    RedisData redisData = new RedisData(LocalDateTime.now().plusSeconds(10),shop);
    redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+1,redisData);
}

步骤三:正式代码

ShopServiceImpl

@Autowired
private RedisTemplate redisTemplate;
//线程池对象
private ExecutorService threadPool = new ThreadPoolExecutor(10,10,10, TimeUnit.SECONDS,
                                                            new ArrayBlockingQueue<>(50)
                                                           );
@Override
public Result queryById(Long id) {
    //1.从redis里面获取商户信息。RedisData的数据
    String key = RedisConstants.CACHE_SHOP_KEY+id;
    RedisData redisData = (RedisData)redisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (ObjectUtil.isEmpty(redisData)) {
        // 不存在,直接返回
        return null;
    }
    //3.判断是否过期。
    if(LocalDateTime.now().isAfter(redisData.getExpireTime())){
        //4.如果过期新开一个线程到数据库里面获取数据
        //加个锁,只允许一个线程到数据库里面获取数据  setnx  not exist
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, 1,10,TimeUnit.SECONDS);
        if(success) {
            threadPool.submit(() -> {
                try {
                    //3.1到mysql获取shop数据
                    Shop shop = getById(id);
                    //3.2把shop包装成RedisData //toDo 为了测试先设置30s,测试完毕后要改为30分钟
                    RedisData rd = new RedisData(LocalDateTime.now().plusSeconds(RedisConstants.CACHE_SHOP_TTL), shop);
                    //3.3把RedisData放入到redis里面
                    redisTemplate.opsForValue().set(key, rd);
                }finally {
                    redisTemplate.delete(lockKey);
                }
            });
        }
    }
    //4.返回data数据
    return Result.ok(redisData.getData());
}

1.5 封装Redis工具类

将逻辑进行封装

//缓存管理的工具类
@Component
public class CacheClient {
    @Autowired
    private RedisTemplate redisTemplate;
    //线程池对象,因为CacheClient类被spring管理了,所以该属性也只有一个。
    private ExecutorService threadPool = new ThreadPoolExecutor(10,10,10, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(50)
    );
    /**
     * 利用互斥锁到缓存里面查询数据,避免缓存击穿,如果缓存没有要到数据库查,并放入缓存
     * @param redisCachePrefix 缓存数据的前缀
     * @param t 查询条件
     * @param redisLockPrefix 锁标记的前缀
     * @param ttl 缓存时长
     * @param function 当缓存过期时如何查询该数据,把查询代码封装到function里面
     * @param <R> 要缓存的对象类型
     * @param <T> 查询条件的类型
     * @return 要缓存的对象
     */
    public <R,T> R queryWithLogicalExpire(String redisCachePrefix,T t,String redisLockPrefix,Long ttl,Function<T,R> function){
        //1.从redis里面获取商户信息。RedisData的数据
        String key = RedisConstants.CACHE_SHOP_KEY+t;
        RedisData redisData = (RedisData)redisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (ObjectUtil.isEmpty(redisData)) {
            // 不存在,直接返回
            return null;
        }
        //3.判断是否过期。
        if(LocalDateTime.now().isAfter(redisData.getExpireTime())){
            //4.如果过期新开一个线程到数据库里面获取数据
            //加个锁,只允许一个线程到数据库里面获取数据  setnx  not exist
            String lockKey = redisLockPrefix + t;
            Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, 1,ttl,TimeUnit.MINUTES);
            if(success) {
                threadPool.submit(() -> {
                    try {
                        //3.1到mysql获取shop数据
                        R newObj = function.apply(t);
                        //3.2把shop包装成RedisData //toDo 为了测试先设置30s,测试完毕后要改为30分钟
                        RedisData rd = new RedisData(LocalDateTime.now().plusMinutes(ttl), newObj);
                        //3.3把RedisData放入到redis里面
                        redisTemplate.opsForValue().set(key, rd);
                    }finally {
                        redisTemplate.delete(lockKey);
                    }
                });
            }
        }
        return (R)redisData.getData();
    }
    /**
     * 利用互斥锁到缓存里面查询数据,避免缓存击穿,如果缓存没有要到数据库查,并放入缓存
     * @param t 查询条件
     * @param <R> 要缓存的对象类型
     * @param <T> 查询条件的类型
     * @return 要缓存的对象
     */
    public <R,T> R queryWithMutex(T t, String redisPrefix, Function<T,R> function,Long ttl,TimeUnit unit){
        //1.先到redis里面获取数据看是否能获取到。
        //1.1生成key
        String key = redisPrefix+t;
        //1.2 根据key获取shop对象
        R r = (R)redisTemplate.opsForValue().get(key);
        if(ObjectUtil.isNotEmpty(r)){
            //2.如果拿到了,直接返回数据。
            return r;
        }
        //谁获取到锁谁到数据库里面取数据。
        synchronized (this) {
            //到redis里面获取商户信息
            r = (R)redisTemplate.opsForValue().get(key);
            if(ObjectUtil.isNotEmpty(r)){
                //2.如果拿到了,直接返回数据。
                return r;
            }
            //3.如果没有拿到,调用数据库获取数据。
            r = function.apply(t);
            if (ObjectUtil.isEmpty(r)) {
                return null;
            }
            //4.再把数据库中拿到的数据放入到redis里面。
            redisTemplate.opsForValue().set(key, r, ttl, unit);
            return r;
        }
    }
}

在ShopServiceImpl 中

@Resource
private CacheClient cacheClient;

 @Override
    public Result queryById(Long id) {
        // 互斥锁解决缓存击穿
        Shop shop = cacheClient.queryWithMutex(id, RedisConstants.CACHE_SHOP_KEY,
                this::getById
        , RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 逻辑过期解决缓存击穿
        // Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, RedisConstants.LOCK_SHOP_KEY, RedisConstants.CACHE_SHOP_TTL, this::getById);
        

        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }

1.6、缓存更新

方式一:
	直接覆盖
	优点:简单
	缺点:时机不好约定
方式二:
	给key规定过期时间
	优点:更新时间好规定
	缺点:容易在高峰期内过期,重新构建缓存耗时久,而且需要经常构建缓存
方式三:
	查询时,添加缓存,不规定过期时间,在对mysql修改时,一律删除缓存,等待查询时重新添加
	优点:保证数据一定是最新的
	缺点:添加和清除次数可能过多,影响效率
	
  • 33
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值