Redis查询缓存

什么是缓存?

缓存是一种提高数据访问效率的技术,通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果,以加快响应速度。

添加Redis缓存有什么好处? 

Redis 基于内存存储,读写速度极快,相比于传统的磁盘存储方式,能够显著提高系统的响应速度。

缓存了高频访问的数据后,可以减少对数据库的访问次数,从而减轻数据库的负载。

简单的将数据存入Redis之后存在什么问题?

缓存一致性问题

数据库中的数据发生变化时,缓存中的数据可能没有及时更新,导致数据不一致。

解决办法:使用缓存更新策略,如主动更新(推荐)、延迟双删、或设置缓存失效时间。

缓存穿透

如果客户端频繁请求数据库中不存在的数据,而这些数据不会被缓存,最终所有请求都会直接打到数据库,增加负载。

解决办法:为不存在的数据设置一个短时间的空值缓存。

缓存雪崩

当大量缓存数据同时过期或 Redis 宕机时,所有请求涌向数据库,可能导致系统崩溃。

解决办法:为不同缓存设置不同的过期时间(分布式过期时间),并配置缓存服务的高可用性(如主从、集群)。

缓存击穿

某个热点数据突然失效时,大量请求直接打到数据库,可能导致数据库压力骤增。

解决办法:使用互斥锁机制,确保缓存重新加载时只有一个请求访问数据库。或者使用逻辑过期方式。这两种需要根据情况来选择。如果需要确保数据的一致性推荐使用互斥锁机制。如果短暂的数据不一致不要紧,追求的是访问速度,推荐使用逻辑过期方式

缓存更新策略

为什么读操作未命中Redis,读数据库存入Redis还需要设置超时时间呢?

通过设置超时时间,即使缓存更新失败,缓存数据会在过期时间后自动失效,重新加载最新数据,确保数据最终一致性。也就是兜底方案。

为什么写的操作是先写入数据库,再删除缓存呢? 

1. 避免读写竞争问题

如果先删除缓存再写数据库,可能出现以下情况:

  • 线程 A 删除缓存。
  • 线程 B 查询数据时发现缓存为空,去数据库查询旧数据,并将其重新写入缓存。
  • 线程 A 写入新的数据到数据库。

这样,缓存中保存的就会是旧数据,导致数据不一致。

为什么先写入数据库就不存在这种读写竞争问题?

因为对Redis的读写时间耗时很短在很短的时间内CPU的执行权被抢夺的概率很小,但是更新数据库的操作比较久,尤其涉及到多表查询更新的时候。被抢夺CPU执行权的概率比较大。

2. 确保缓存数据最终一致

先写入数据库再删除缓存的顺序,可以保证以下情况:

  • 数据库中总是有最新的数据。
  • 即使缓存被删除后有线程查询,触发缓存更新时,查询到的也是最新的数据。
package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    final StringRedisTemplate stringRedisTemplate;
    /**
     * 实现Redis缓存店铺信息
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 1. 首先从Redis当中查询是否存在商铺
            // 存储对象可以使用HashMap 也可以使用string类型
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 存在直接返回Redis中的信息
        if (StrUtil.isNotBlank(shopStr)) {
            // 将string类型转换为对象
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }

        // 不存在,则查询数据库
        Shop shop = getById(id);
        if (shop == null) {
            // 数据库不存在返回404
            return Result.fail("商铺不存在!");
        }
        // 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 返回商铺信息
        return Result.ok(shop);
    }

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

 缓存穿透(访问不存在的数据)

也就是说我们在之前的查询的代码上,还需要添加如果Redis当中存在值且不为空直接返回Redis中的数据。还需要判断Redis存储的数据是否wield空 如果为空直接返回错误信息(不需要再查询数据库,导致数据库崩溃)。如果Redis当中的值为null我们需要查询数据库 如果数据库查询不到,我们需要将这个不存在的数据保存在Redis,值为空(防止缓存击穿),并且设置TTL

判断 Redis 是否存储了空值

if (shopStr != null) 判断了 Redis 中的值是否为 ,如果为 "" 或空值标记,表示这是防止缓存穿透写入的特殊数据,直接返回错误信息。

数据库查询为空时缓存空值

使用 "" 或其他标记作为空值,写入 Redis,并设置一个较短的过期时间(例如 2 分钟)。这样可以避免频繁查询数据库,防止缓存穿透。

正常数据的缓存设置 TTL

如果数据库中有数据,写入 Redis 时设置长时间的 TTL(如 CACHE_SHOP_TTL),确保数据一致性。

更新数据后删除缓存

确保先更新数据库,再删除缓存,避免并发情况下的数据不一致问题。

package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    final StringRedisTemplate stringRedisTemplate;

    /**
     * 实现Redis缓存店铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 1. 从Redis中查询商铺信息
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 2. 如果Redis中存在值且不为空字符串,直接返回
        if (StrUtil.isNotBlank(shopStr)) {
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }

        // 3. 判断Redis中是否存储了空值,防止缓存穿透
        if (shopStr != null) {
            return Result.fail("商铺不存在!");
        }

        // 4. Redis中不存在数据,从数据库查询
        Shop shop = getById(id);

        // 5. 如果数据库中也不存在,则写入一个空值到Redis,并设置短TTL,防止缓存穿透
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(
                    CACHE_SHOP_KEY + id, 
                    "", // 空值标记
                    2, // 短时间TTL(如2分钟)
                    TimeUnit.MINUTES
            );
            return Result.fail("商铺不存在!");
        }

        // 6. 如果数据库中存在,将商铺信息写入Redis,并设置TTL
        stringRedisTemplate.opsForValue().set(
                CACHE_SHOP_KEY + id,
                JSONUtil.toJsonStr(shop),
                CACHE_SHOP_TTL,
                TimeUnit.MINUTES
        );

        // 7. 返回商铺信息
        return Result.ok(shop);
    }

    /**
     * 更新商铺信息
     *
     * @param shop
     * @return
     */
    @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_KEY + id);
        return Result.ok();
    }
}

缓存雪崩 

 

缓存击穿问题:(数据过期同时有大量请求)

 缓存击穿问题发生在某些热点数据在缓存失效的瞬间,同时有大量请求涌入,从而导致这些请求直接访问数据库,可能会引发数据库压力过大甚至宕机的情况。

1. 给线程加锁(互斥锁)

原理:

通过对热点数据的访问加互斥锁(如分布式锁或本地锁),保证在缓存失效后,只有一个线程能去查询数据库并更新缓存,其余线程等待锁释放后从缓存读取数据。

优点:
  1. 简单易实现:逻辑清晰,只需引入锁机制即可。
  2. 保护数据库:有效防止多线程同时查询数据库,降低数据库压力。
  3. 通用性强:适用于所有数据场景,尤其是并发量高的热点数据。
缺点:
  1. 性能问题
    • 在高并发情况下,锁的排队等待会增加响应时间。
    • 如果锁竞争激烈,会导致线程阻塞。
  2. 单点问题
    • 如果锁是本地实现,可能会出现分布式环境中的一致性问题。
    • 使用分布式锁(如 Redis 分布式锁)会增加实现复杂度。
  3. 潜在死锁风险:如果锁机制设计不当,可能会导致死锁。

使用互斥锁的代码逻辑:

查询 Redis:

  • 首先查询 Redis 是否存在目标数据。
  • 如果命中,直接返回。
  • 如果未命中(Redis 中没有目标数据),进入下一步。
  • 尝试获取锁:

如果获取锁成功:

  1. 再次查询 Redis 是否有数据(防止其他线程已加载数据)。
  2. 如果 Redis 中仍然没有数据,查询数据库,加载数据到 Redis。
  3. 释放锁,返回数据。

如果未获取到锁:

  1. 等待一段时间后,重复尝试获取锁。
  2. 在每次尝试获取锁时,先查询 Redis 是否已存在数据,防止无意义的锁竞争。

为什么获取锁之后还需要再次检查Redis中是否存在数据?

核心原因: 避免重复查询数据库和重复写入 Redis,从而减少资源浪费。

多线程场景下的问题:

  • 假设线程 A 获取到锁并完成了缓存重建(从数据库查询数据并写入 Redis)。
  • 线程 B 在等待锁的过程中,其实线程 A 已经完成了数据的缓存。
  • 线程 B 获取到锁时,如果不再次检查 Redis,就会重复从数据库查询并覆盖 Redis 中的已有数据,导致不必要的开销和延迟

这个锁应该是什么样的锁?是平常的吗 

 使用 Redis 实现分布式锁

Redis 是实现分布式锁的常用工具。通过 SETNXset if not exists)命令以及锁的过期时间,可以实现高效且可靠的分布式锁。

  • 如果键不存在,SETNX 会创建这个键并返回成功(true)。
  • 如果键已经存在,SETNX 会直接返回失败(false)。
/**
 * 尝试获取锁
 * @param key 锁的唯一标识
 * @return 是否获取成功
 */
private boolean tryLock(String key) {
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(success);
}

/**
 * 释放锁
 * @param key 锁的唯一标识
 */
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    final StringRedisTemplate stringRedisTemplate;

    /**
     * 查询商铺信息,包含缓存击穿的解决方案
     */
    @Override
    public Result queryById(Long id) {
        // 通过互斥锁解决缓存击穿问题
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        return Result.ok(shop);
    }

    /**
     * 互斥锁解决缓存击穿
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        String lockKey = LOCK_SHOP_KEY + id;
        
        // 1. 查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        if (shopJson != null) {
            return null; // 空值
        }

        Shop shop = null;
        boolean isLockAcquired = false;

        try {
            // 2. 尝试获取锁
            while (!(isLockAcquired = tryLock(lockKey))) {
                Thread.sleep(50); // 未获取锁,等待并重试
            }

            // 3. 再次检查缓存,防止重复查询数据库
            String shopJsonAfterLock = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopJsonAfterLock)) {
                return JSONUtil.toBean(shopJsonAfterLock, Shop.class);
            }
            if (shopJsonAfterLock != null) {
                return null; // 空值
            }

            // 4. 查询数据库
            shop = getById(id);
            if (shop == null) {
                // 数据库不存在,写入空值防止缓存穿透
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            // 5. 写入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复线程中断状态
            throw new RuntimeException("线程被中断", e);
        } finally {
            // 6. 释放锁
            if (isLockAcquired) {
                unlock(lockKey);
            }
        }

        return shop;
    }

    /**
     * 获取锁
     */
    private boolean tryLock(String key) {
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(success);
    }

    /**
     * 释放锁(防止误删其他线程的锁)
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 更新商铺信息,并清除缓存
     */
    @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_KEY + id);

        return Result.ok();
    }
}

2. 逻辑过期

原理:

将缓存数据设置为逻辑过期时间,同时保留旧值。当缓存过期时,请求仍然返回旧数据,同时后台异步更新缓存(通过任务队列或线程池刷新数据)。

优点:
  1. 高性能
    • 由于返回旧数据,请求不会直接打到数据库,避免了数据库的压力。
    • 不会阻塞用户线程,用户体验更好。
  2. 无锁机制
    • 通过异步任务更新缓存,避免了加锁的复杂性和性能问题。
  3. 支持高并发
    • 用户请求不会因缓存失效而阻塞。
缺点:
  1. 数据一致性问题
    • 在缓存逻辑过期的时间段内,可能返回的是旧数据,适合对一致性要求不高的场景。
  2. 实现复杂度高
    • 需要设计异步更新逻辑,增加系统复杂性。
    • 需要引入额外的定时任务或异步线程池处理刷新逻辑。
  3. 资源占用
    • 异步更新的任务可能会带来额外的资源消耗,尤其是在数据量较大时。

选择建议

  1. 互斥锁适合对数据一致性要求高的业务场景,如订单、库存等核心数据,且并发量较低。
  2. 逻辑过期更适合高并发、热点数据且对数据一致性要求不高的场景,如新闻热点、排行榜等。

逻辑过期方式解决问题的逻辑:

查询缓存数据

  • 通过Rediskey查询数据,结果可能是:
    • 缓存未命中:直接返回null
    • 缓存命中:继续处理逻辑。

判断逻辑过期

  • 未过期:直接返回缓存中的数据。
  • 已过期:需要重建缓存。
  • 重建缓存(防止缓存穿透)

获取分布式锁(互斥锁)。

  • 如果获取不到锁:
    • 直接返回旧的数据,避免阻塞等待。
  • 如果获取到锁:
    • 再次检查是否过期(双重检查锁机制),防止锁释放期间其他线程已更新数据。
    • 开启一个独立线程完成缓存重建,释放锁后返回旧数据。

缓存重建逻辑

  • 查询数据库获取最新数据。
  • 写入Redis,并设置逻辑过期时间。
  • 释放锁,确保其他线程可以继续执行。
public Shop queryWithLogicExpire(Long id){
        String key = CACHE_SHOP_KEY+id;
        // 1. 首先从Redis当中查询是否存在商铺
        // 存储对象可以使用HashMap 也可以使用string类型
        String shopStr = stringRedisTemplate.opsForValue().get(key);

        // Redis中不存在该信息
        if(StrUtil.isBlank(shopStr)){
            return null;
        }
        RedisData redisData = JSONUtil.toBean(shopStr, RedisData.class);
//        Shop data = (Shop) redisData.getData();
        Shop data = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        // 存在 需要判断缓存是否逻辑过期
             // 未过期 直接返回数据
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
            return data;
        }
        // 过期 缓存重建
        // 尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        if (tryLock(lockKey)) {
            //如果线程b检查完过期时间后,线程a刚好重建完成并释放锁,
            // 此时线程b可以拿到锁并再次重建,所以需要进行二次校验过期时间
            // 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程
            shopStr = stringRedisTemplate.opsForValue().get(key);
            RedisData redisDataAfterGetLock = JSONUtil.toBean(shopStr, RedisData.class);
            if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {
                return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),Shop.class);
            }
            //TODO 获取到了就开启独立的线程,
            CATHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    // 重建缓存
                    this.saveShop2Redis(id,CACHE_SHOP_TTL);
                } catch (Exception e) {
                    log.error("缓存重建失败", e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        //  还是返回旧的数据
        return data;

为什么需要进行二次校验?

以下是可能的线程执行时间线:

  1. 线程A:发现缓存过期,获取锁,进入重建逻辑。
  2. 线程B:也发现缓存过期,但未获取到锁,进入等待状态。
  3. 线程A:完成缓存重建,更新Redis数据,释放锁。
  4. 线程B:获取到锁,继续执行(认为缓存仍然过期)。
    • 如果没有二次校验,线程B会再次重建缓存。
    • 如果有二次校验,线程B会发现缓存数据已经更新,不需要重复重建。

此时,如果线程A完成缓存重建并释放锁后,线程B再次获取锁,直接重建缓存,会造成:

  1. 重复的缓存重建(浪费资源)。
  2. 数据被多次写入Redis,增加Redis的负担。

二次校验通过在获取锁后,再次检查缓存数据是否过期(或已经被其他线程更新),可以避免这种重复操作。

为什么没有查到数据的时候直接返回null?

因为对于热点key来说,我们会先将数据存储到Redis当中,并且设置逻辑过期时间。如果根据key在Redis当中查询不到该数据,只能说明这个数据不在热点当中,直接返回空即可。

未命中说明:

  • key不属于热点数据或从未被缓存。
  • 逻辑过期方案主要针对热点数据的缓存维护,对非热点数据无需增加负担。

如何设置逻辑超时时间?

引入一个RedisData类,包含以下字段:

  • expireTime:逻辑过期时间(LocalDateTime类型)。
  • data:具体的业务数据对象(如Shop类)
package com.hmdp.utils;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

缓存数据存储到Redis

RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(CACHE_SHOP_TTL));
redisData.setData(shop); // shop 是业务对象
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));


使用JSON序列化将RedisData对象保存到Redis中: 

封装成工具类 

 

set:普通的缓存设置方法。

setWithLogicExpire:设置逻辑过期缓存。

queryWithPassThrough:解决缓存穿透问题。

queryWithLogicExpire:解决缓存击穿问题(使用逻辑过期方案)。

需要注意两个点:

提高通用性:面向多个类封装工具包的设计原则

使用泛型:

当工具包需要支持多个类时,提高通用性是关键。可以通过以下方式实现:

1.1 使用泛型
  • 为什么使用泛型:不同的调用者可能会涉及不同的数据模型和返回类型。通过泛型,可以让工具方法适配任意类型的返回值,而不需要为每个类重复编写代码。
  • 如何使用泛型
    • 定义返回值类型 R:适配不同的对象类型(如 ShopUser 等)。
    • 定义主键类型 ID:适配不同的数据主键(如 LongString)。
public <R, ID> R queryWithPassThrough(
    String keyPrefix, ID id, Class<R> type, 
    Function<ID, R> dbFallback, Long time, TimeUnit unit
) {
    // 逻辑与实现
}
  • R 代表返回的实体类类型,如 ShopUser 等。
  • ID 代表主键的类型,如 Long(数字型 ID)或 String(UUID)。

2. 如何根据调用者需求获取数据库数据

关键点:调用者可能有不同的需求,例如:

  1. 查询逻辑不同(按 ID、按名称等)。
  2. 数据返回类型不同(单个对象、列表等)。
  3. 数据库表不同。

解决方案: 使用 Function<ID, R> 获取数据

Function<ID, R> 的优势
  1. 灵活性:调用者可以动态传递 Lambda 表达式,实现灵活的查询逻辑。
  2. 解耦:工具类不需要依赖具体的服务或 DAO 实现,而是将查询逻辑交给调用者。
  3. 简单易用:调用者可以用 Lambda 或方法引用直接定义查询逻辑。
工具类的实现
public <R, ID> R queryWithPassThrough(
    String keyPrefix, ID id, Class<R> type,
    Function<ID, R> dbFallback, Long time, TimeUnit unit
) {
    String key = keyPrefix + id;
    String json = stringRedisTemplate.opsForValue().get(key);

    // 判断缓存中是否有数据
    if (StrUtil.isNotBlank(json)) {
        return JSONUtil.toBean(json, type); // 缓存命中,返回数据
    }

    // 查询数据库
    R result = dbFallback.apply(id);
    if (result == null) {
        // 防止缓存穿透,缓存空值
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }

    // 数据库有数据,写入缓存
    this.set(key, result, time, unit);
    return result;
}
调用示例

使用 Lambda 表达式或方法引用定义数据库查询逻辑:

// Lambda 表达式
queryWithPassThrough(
    "shop:", 1L, Shop.class,
    id -> shopService.findById(id),
    10L, TimeUnit.MINUTES
);

// 方法引用
queryWithPassThrough(
    "user:", "abc123", User.class,
    userService::findById,
    10L, TimeUnit.MINUTES
);
package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class CacheClient {
    final StringRedisTemplate stringRedisTemplate;

    public  void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }
    public  void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id,
                                         Class<R> type, Function<ID,R> dbFallback,
                                         Long time, TimeUnit unit){
        // 1. 首先从Redis当中查询是否存在商铺
        // 存储对象可以使用HashMap 也可以使用string类型
        String key = keyPrefix + id;
        String Json = stringRedisTemplate.opsForValue().get(key);

        // 存在直接返回Redis中的信息
        if (StrUtil.isNotBlank(Json)) {
            // 将string类型转换为对象
          return JSONUtil.toBean(Json,type);
        }
        // 判断命中的是否是空值
        if (Json != null) {
            // 是空值,返回一个错误信息
            return null;
        }

        // 不存在,则查询数据库
        R r =dbFallback.apply(id);

        if (r == null) {
            // 数据库不存在返回404
            // 要将空值写入Redis 防止缓存穿透
            stringRedisTemplate.opsForValue().set(keyPrefix +id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }

        // 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案
          this.set(key,r,time,unit);
        // 返回商铺信息
        return r;
    }

    // 线程池
    // 不知道怎么根据id查询数据库?那么就需要面向函数式接口,让调用者传递函数
    private static final ExecutorService CATHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public <R,ID> R queryWithLogicExpire(String prefixKey, String PrefixLock,ID id,
                                         Class<R> type,Function<ID,R> dbFallback,  Long time, TimeUnit unit){
        String key = prefixKey+id;
        // 1. 首先从Redis当中查询是否存在商铺
        // 存储对象可以使用HashMap 也可以使用string类型
        String json = stringRedisTemplate.opsForValue().get(key);

        // Redis中不存在该信息
        if(StrUtil.isBlank(json)){
            return null;
        }
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
//        Shop data = (Shop) redisData.getData();
        R data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        // 存在 需要判断缓存是否逻辑过期
        // 未过期 直接返回数据
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
            return data;
        }
        // 过期 缓存重建
        // 尝试获取互斥锁
        String lockKey = PrefixLock + id;
        if (tryLock(lockKey)) {
            //如果线程b检查完过期时间后,线程a刚好重建完成并释放锁,
            // 此时线程b可以拿到锁并再次重建,所以需要进行二次校验过期时间
            // 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程
            json = stringRedisTemplate.opsForValue().get(key);
            RedisData redisDataAfterGetLock = JSONUtil.toBean(json, RedisData.class);
            if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {
                return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),type);
            }
            //TODO 获取到了就开启独立的线程,
            CATHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    // 重建缓存
                    // 查询数据库
                    R r = dbFallback.apply(id);
                    this.setWithLogicExpire(key,r,time,unit);
                } catch (Exception e) {
                    log.error("缓存重建失败", e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        //  还是返回旧的数据
        return data;
    }
    /**
     * 获取锁
     */
    private boolean tryLock(String key) {
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(success);
    }

    /**
     * 释放锁(防止误删其他线程的锁)
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/950576.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3D立体无人机夜间表演技术详解

3D立体无人机夜间表演技术是一种结合了无人机技术、灯光艺术和计算机编程的创新表演形式。以下是该技术的详细解析&#xff1a; 一、技术基础 1. 无人机技术&#xff1a; 无人机通常采用四旋翼设计&#xff0c;具有强大的飞行控制能力&#xff0c;可以实现前飞、后飞、悬停、…

MATLAB深度学习实战文字识别

文章目录 前言视频演示效果1.DB文字定位环境配置安装教程与资源说明1.1 DB概述1.2 DB算法原理1.2.1 整体框架1.2.2 特征提取网络Resnet1.2.3 自适应阈值1.2.4 文字区域标注生成1.2.5 DB文字定位模型训练 2.CRNN文字识别2.1 CRNN概述2.2 CRNN原理2.2.1 CRNN网络架构实现2.2.2 CN…

H2数据库在单元测试中的应用

H2数据库特征 用比较简洁的话来介绍h2数据库&#xff0c;就是一款轻量级的内存数据库&#xff0c;支持标准的SQL语法和JDBC API&#xff0c;工业领域中&#xff0c;一般会使用h2来进行单元测试。 这里贴一下h2数据库的主要特征 Very fast database engineOpen sourceWritten…

Android 10.0 授权app获取cpu温度和电池温度功能实现

1.前言 在10.0的系统定制化开发中&#xff0c;在开发某些产品的老化应用的时候&#xff0c;需要app获取cpu温度和电池 温度等功能&#xff0c;有些产品带温度传感器&#xff0c;大部分的产品都不包含温度传感器&#xff0c;所以就需要读取 sys下的相关节点来获取相关温度值 2.…

IDEA 撤销 merge 操作(详解)

作为一个开发者&#xff0c;我们都知道Git是一个非常重要的版本控制工具&#xff0c;尤其是在协作开发的过程中。然而&#xff0c;在使用Git的过程中难免会踩一些坑&#xff0c;今天我来给大家分享一个我曾经遇到的问题&#xff1a;在使用IDEA中进行merge操作后如何撤销错误的合…

WD5105同步降压转换器:9.2V-95V宽电压输入,4.5A大电流输出,95%高效率,多重保护功能

概述 • WD5105同步降压转换器 • 封装形式&#xff1a;QFN-20封装 • 应用场景&#xff1a;适用于车载充电器、电动车仪表、电信基站电源、电源适配器等 性能特点 • 输入电压范围&#xff1a;9.2V至95V • 输出电流&#xff1a;可提供4.5A连续负载电流 • 效率&#xff1a;高…

【C++】B2108 图像模糊处理

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述题目内容输入格式输出格式示例输入&#xff1a;输出&#xff1a; &#x1f4af;题目分析问题拆解 &#x1f4af;我的做法代码实现代码分析 &#x1f4af;老师的做法…

怎么把word试题转成excel?

在教育行业、学校管理以及在线学习平台中&#xff0c;试题库的高效管理是一项核心任务。许多教育工作者和系统开发人员常常面临将 Word 中的试题批量导入 Excel 的需求。本文将详细介绍如何快速将试题从 Word 转换为 Excel&#xff0c;帮助您轻松解决繁琐的数据整理问题&#x…

minibatch时,损失如何记录

目录 minibatch时&#xff0c;损失如何记录 报错&#xff1a;UnboundLocalError: local variable coef referenced before assignment是什么回事 未溢出则不会报错&#xff0c;可以完整滴运行完成 indent 缩进 炫酷技能&#xff1a;一遍运行&#xff0c;一遍画图 实例1 解释…

Linux : Linux环境开发工具vim / gcc / makefile / gdb / git的使用

Linux环境开发工具的使用 一、操作系统的生态二、程序下载安装&#xff08;一&#xff09;程序安装方式&#xff08;二&#xff09;包管理器 yum / apt 运行原理 三、文本编辑器 vim&#xff08;一&#xff09;认识vim 下的操作模式&#xff08;二&#xff09;命令模式常用的快…

国产游戏崛起,燕云十六移动端1.9上线,ToDesk云电脑先开玩

游戏爱好者的利好消息出新了&#xff01;网易大型武侠仙游《燕云十六声》正式官宣&#xff0c;移动端要在1月9日正式上线了&#xff01;你期待手游版的燕云吗&#xff1f;不妨评论区留言说说你的看法。小编分别花了几个小时在台式机电脑和手机上都试了下&#xff0c;欣赏画面还…

力扣刷题:数组OJ篇(下)

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 目录 1.轮转数组&#xff08;1&#xff09;题目描述…

有序数据中插入不确定数据保证数据插入的位置顺序正确排序

解决有序数据中插入不确定数据保证数据插入的位置顺序正确排序 前言 java 数据库中存储自增id 有序的数据&#xff0c; 前端页面基于 id 5和 6 之间新增一条数据&#xff0c;在 id 6 和 7之间新增 2条&#xff0c;或者更复杂的场景&#xff0c;后台接口如何保存数据使得页面数…

python无需验证码免登录12306抢票 --selenium(2)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 [TOC](python无需验证码免登录12306抢票 --selenium(2)) 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 就在刚刚我抢的票&#xff1a;2025年1月8日…

DNS协议漏洞利用实验_hust计算机网络安全实验

文章目录 计算机网络安全实验 DNS协议漏洞利用实验 docker使用 建立实验环境docker常用指令 一些注意事项设置本地 DNS 服务器 配置用户计算机设置本地DNS服务器在本地 DNS 服务器中建一个区域 修改主机文件&#xff08;可略&#xff09;netwox实施DNS的用户响应欺骗攻击netwo…

基于MP157AAA的I2C练习

练习要求&#xff1a; 通过I2C分别实现与芯片si7006(获取湿度、温度)和芯片ap3216(获取环境光照强度)的通讯&#xff1b; 1、运行效果 2、分析ap3216如何获取光照强度 2.1、需要操作的寄存器 通过分析手册&#xff0c;需要操作以下寄存器: 0x00&#xff1a;系统配置 0x0C&…

【Linux】深入理解文件系统(超详细)

目录 一.磁盘 1-1 磁盘、服务器、机柜、机房 &#x1f4cc;补充&#xff1a; &#x1f4cc;通常网络中用高低电平&#xff0c;磁盘中用磁化方向来表示。以下是具体说明&#xff1a; &#x1f4cc;如果有一块磁盘要进行销毁该怎么办&#xff1f; 1-2 磁盘存储结构 ​编辑…

网络安全图谱以及溯源算法

​ 本文提出了一种网络攻击溯源框架&#xff0c;以及一种网络安全知识图谱&#xff0c;该图由六个部分组成&#xff0c;G <H&#xff0c;V&#xff0c;A&#xff0c;E&#xff0c;L&#xff0c;S&#xff0c;R>。 1|11.知识图 ​ 网络知识图由六个部分组成&#xff0c…

《Spring Framework实战》7:4.1.2.容器概述

欢迎观看《Spring Framework实战》视频教程 容器概述 该接口表示 Spring IoC 容器&#xff0c;并负责实例化、配置和组装 bean。 容器在组件上获取其指令&#xff0c;以实例化、配置和 通过读取配置元数据进行汇编。可以表示配置元数据 作为带注释的组件类、具有工厂方法的配置…

学生公寓技术规格书如何编写?

学生公寓限电柜的技术规格书主要包括以下内容‌&#xff1a; ‌用电计量计费‌&#xff1a;限电柜可以通过计算机售电管理系统进行用电计量计费&#xff0c;学生需要预交电费&#xff0c;系统会自动将数据传给控电柜和配电箱&#xff0c;对宿舍的电量进行累减计量‌。 ‌时间控…