Redis实战篇|将解决缓存问题方案封装成工具类

目录

封装代码

缓存穿透解决方案

缓存击穿解决方案

逻辑过期策略

互斥锁策略

其余代码

缓存数据结构

使用示例

代码解释

定义缓存枚举策略

步骤 1:定义缓存策略枚举

步骤 2:封装参数对象

步骤 3:在 CacheClient 中实现统一查询入口

步骤 4:调用示例

设计优势

关键问题

原理分析


封装代码

封装一个类似的工具类 CacheClient,该工具类可以处理缓存击穿、缓存雪崩等问题,包含缓存穿透、缓存击穿(逻辑过期)的解决方案。

缓存穿透解决方案

/**
     * 缓存穿透解决方案
     * @param keyPrefix 缓存键前缀
     * @param id 数据 ID
     * @param type 数据类型
     * @param dbFallback 数据库查询函数
     * @param time 缓存时间
     * @param unit 时间单位
     * @param <R> 返回数据类型
     * @param <ID> 数据 ID 类型
     * @return 数据对象
     */
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 从 Redis 查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否存在
        if (json != null) {
            // 存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if ("".equals(json)) {
            // 返回错误信息
            return null;
        }
        // 不存在,根据 ID 查询数据库
        R r = dbFallback.apply(id);
        // 不存在,将空值写入 Redis
        if (r == null) {
            // 将空值写入 Redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 存在,写入 Redis
        this.set(key, r, time, unit);
        return r;
    }

缓存击穿解决方案

  1. 逻辑过期策略
    /**
         * 缓存击穿解决方案(逻辑过期)
         * @param keyPrefix 缓存键前缀
         * @param id 数据 ID
         * @param type 数据类型
         * @param dbFallback 数据库查询函数
         * @param expireTime 逻辑过期时间
         * @param <R> 返回数据类型
         * @param <ID> 数据 ID 类型
         * @return 数据对象
         */
        public <R, ID> R queryWithLogicalExpire(
                String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime) {
            String key = keyPrefix + id;
            // 1. 从 Redis 查询缓存
            String json = stringRedisTemplate.opsForValue().get(key);
            // 2. 判断是否存在
            if (json == null) {
                // 3. 不存在,直接返回空
                return null;
            }
            // 4. 命中,需要先把 JSON 反序列化为对象
            RedisData redisData = JSONUtil.toBean(json, RedisData.class);
            R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
            LocalDateTime expireTime1 = redisData.getExpireTime();
            // 5. 判断是否过期
            if (expireTime1.isAfter(LocalDateTime.now())) {
                // 5.1 未过期,直接返回店铺信息
                return r;
            }
            // 5.2 已过期,需要缓存重建
            // 6. 缓存重建
            // 6.1 获取互斥锁
            String lockKey = LOCK_SHOP_KEY + id;
            boolean isLock = tryLock(lockKey);
            // 6.2 判断是否获取锁成功
            if (isLock) {
                // 6.3 成功,开启独立线程,实现缓存重建
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        // 查询数据库
                        R newR = dbFallback.apply(id);
                        // 重建缓存
                        this.setWithLogicalExpire(key, newR, expireTime);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        // 释放锁
                        unlock(lockKey);
                    }
                });
            }
            // 6.4 返回过期的店铺信息
            return r;
        }

  2. 互斥锁策略
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, 
            Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1. 查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) return JSONUtil.toBean(json, type);
    ​
        // 2. 缓存未命中,尝试获取锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r;
        try {
            if (!tryLock(lockKey)) {
                // 2.1 未获取到锁,休眠后重试
                Thread.sleep(50);
                return queryWithMutex(...); // 递归调用
            }
    ​
            // 2.2 获取到锁,二次检查缓存(防止其他线程已重建)
            json = stringRedisTemplate.opsForValue().get(key);
            if (json != null) return JSONUtil.toBean(json, type);
    ​
            // 3. 查询数据库
            r = dbFallback.apply(id);
            if (r == null) {
                // 缓存空值(防止穿透)
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
    ​
            // 4. 写入缓存
            set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unlock(lockKey);
        }
        return r;
    }

    其余代码

@Component
public class CacheClient {
​
    private final StringRedisTemplate stringRedisTemplate;
​
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {...}
    public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, 
        Function<ID, R> dbFallback, Long expireTime) {...}
    public <R, ID> R queryWithMutex(
        String keyPrefix, ID id, Class<R> type, 
        Function<ID, R> dbFallback, Long time, TimeUnit unit){...}
    /**
     * 设置缓存
     * @param key 缓存键
     * @param value 缓存值
     * @param time 缓存时间
     * @param unit 时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
​
    /**
     * 设置带有逻辑过期时间的缓存
     * @param key 缓存键
     * @param value 缓存值
     * @param expireSeconds 逻辑过期时间(秒)
     */
    public void setWithLogicalExpire(String key, Object value, Long expireSeconds) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 写入 Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
​
    /**
     * 尝试获取锁
     * @param key 锁键
     * @return 是否获取成功
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
​
    /**
     * 释放锁
     * @param key 锁键
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
​
缓存数据结构
public static class RedisData {
    private LocalDateTime expireTime;
    private Object data;
​
    public LocalDateTime getExpireTime() {
        return expireTime;
    }
​
    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
​
    public Object getData() {
        return data;
    }
​
    public void setData(Object data) {
        this.data = data;
    }
}

使用示例

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
​
    @Autowired
    private CacheClient cacheClient;
​
    @Override
    public Result queryById(Long id) {
        // 使用缓存穿透解决方案
        Shop shop = cacheClient.queryWithPassThrough(
                CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
​
        // 或者使用缓存击穿(逻辑过期)解决方案
        // Shop shop = cacheClient.queryWithLogicalExpire(
        //         CACHE_SHOP_KEY, id, Shop.class, this::getById, 10L);
​
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }
}

代码解释

  1. queryWithPassThrough 方法:用于解决缓存穿透问题,先从 Redis 查询缓存,若缓存为空则查询数据库,若数据库也不存在则将空值写入 Redis 缓存,避免后续请求重复查询数据库。

  2. queryWithLogicalExpire 方法:用于解决缓存击穿问题,使用逻辑过期策略,当缓存过期时,尝试获取互斥锁,获取成功则开启独立线程重建缓存,当前请求返回旧数据。

  3. queryWithMutex方法:用于解决缓存击穿问题,使用互斥锁策略,当缓存过期时,尝试获取互斥锁,在互斥锁释放前阻塞其他线程,保证数据的强一致性

  4. set 方法:用于设置普通缓存,包含缓存键、缓存值、缓存时间和时间单位。

  5. setWithLogicalExpire 方法:用于设置带有逻辑过期时间的缓存,将数据和逻辑过期时间封装到 RedisData 对象中。

  6. tryLockunlock 方法:用于获取和释放互斥锁,保证缓存重建的线程安全。

定义缓存枚举策略

步骤 1:定义缓存策略枚举

枚举中每个常量对应一种策略,并通过抽象方法统一执行逻辑。

public enum CacheStrategy {
    PASS_THROUGH {
        @Override
        public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
            // 参数校验
            if (params.time == null || params.unit == null) {
                throw new IllegalArgumentException("PASS_THROUGH 策略需设置 time 和 unit");
            }
            return client.queryWithPassThrough(
                params.keyPrefix, params.id, params.type, 
                params.dbFallback, params.time, params.unit
            );
        }
    },
    LOGICAL_EXPIRE {
        @Override
        public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
            if (params.expireTime == null) {
                throw new IllegalArgumentException("LOGICAL_EXPIRE 策略需设置 expireTime");
            }
            return client.queryWithLogicalExpire(
                params.keyPrefix, params.id, params.type, 
                params.dbFallback, params.expireTime
            );
        }
    },
    MUTEX_LOCK {
        @Override
        public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
            if (params.time == null || params.unit == null || params.lockTimeout == null) {
                throw new IllegalArgumentException("MUTEX_LOCK 需设置 time, unit 和 lockTimeout");
            }
            return client.queryWithMutex(
                params.keyPrefix, params.id, params.type, 
                params.dbFallback, params.time, params.unit, params.lockTimeout
            );
        }
    };
​
    // 抽象方法,由每个枚举实现具体策略
    public abstract <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params);
}

步骤 2:封装参数对象

使用建造者模式创建参数对象,灵活支持不同策略的参数需求。

public class CacheQueryParams<ID, R> {
    private final String keyPrefix;
    private final ID id;
    private final Class<R> type;
    private final Function<ID, R> dbFallback;
    private Long time;          // 用于 PASS_THROUGH/MUTEX_LOCK
    private TimeUnit unit;      // 用于 PASS_THROUGH/MUTEX_LOCK
    private Long expireTime;    // 用于 LOGICAL_EXPIRE
    private Long lockTimeout;   // 用于 MUTEX_LOCK
​
    // 私有构造方法,通过 Builder 构建
    private CacheQueryParams(Builder<ID, R> builder) {
        this.keyPrefix = builder.keyPrefix;
        this.id = builder.id;
        this.type = builder.type;
        this.dbFallback = builder.dbFallback;
        this.time = builder.time;
        this.unit = builder.unit;
        this.expireTime = builder.expireTime;
        this.lockTimeout = builder.lockTimeout;
    }
​
    // 静态内部建造者类
    public static class Builder<ID, R> {
        private String keyPrefix;
        private ID id;
        private Class<R> type;
        private Function<ID, R> dbFallback;
        private Long time;
        private TimeUnit unit;
        private Long expireTime;
        private Long lockTimeout;
​
        public Builder<ID, R> keyPrefix(String keyPrefix) {
            this.keyPrefix = keyPrefix;
            return this;
        }
​
        public Builder<ID, R> id(ID id) {
            this.id = id;
            return this;
        }
​
        public Builder<ID, R> type(Class<R> type) {
            this.type = type;
            return this;
        }
​
        public Builder<ID, R> dbFallback(Function<ID, R> dbFallback) {
            this.dbFallback = dbFallback;
            return this;
        }
​
        public Builder<ID, R> time(Long time) {
            this.time = time;
            return this;
        }
​
        public Builder<ID, R> unit(TimeUnit unit) {
            this.unit = unit;
            return this;
        }
​
        public Builder<ID, R> expireTime(Long expireTime) {
            this.expireTime = expireTime;
            return this;
        }
​
        public Builder<ID, R> lockTimeout(Long lockTimeout) {
            this.lockTimeout = lockTimeout;
            return this;
        }
​
        public CacheQueryParams<ID, R> build() {
            return new CacheQueryParams<>(this);
        }
    }
}

步骤 3:在 CacheClient 中实现统一查询入口

提供 query 方法,根据策略和参数执行对应逻辑。

@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;
​
    // 其他代码...
​
    public <R, ID> R query(
        CacheStrategy strategy, 
        CacheQueryParams<ID, R> params
    ) {
        return strategy.execute(this, params);
    }
​
    // 原有方法保持不变
    public <R, ID> R queryWithPassThrough(
        String keyPrefix, ID id, Class<R> type, 
        Function<ID, R> dbFallback, Long time, TimeUnit unit
    ) { /* 原有逻辑 */ }
​
    public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, 
        Function<ID, R> dbFallback, Long expireTime
    ) { /* 原有逻辑 */ }
​
    public <R, ID> R queryWithMutex(
        String keyPrefix, ID id, Class<R> type, 
        Function<ID, R> dbFallback, Long time, TimeUnit unit, Long lockTimeout
    ) { /* 新增互斥锁方案 */ }
}

步骤 4:调用示例

调用者通过建造者设置参数,并指定策略执行查询。

// 创建参数对象
CacheQueryParams<Long, Shop> params = new CacheQueryParams.Builder<Long, Shop>()
    .keyPrefix("shop:")
    .id(1L)
    .type(Shop.class)
    .dbFallback(id -> shopMapper.selectById(id))
    .expireTime(30L)      // LOGICAL_EXPIRE 所需
    .time(30L)            // PASS_THROUGH/MUTEX_LOCK 所需
    .unit(TimeUnit.MINUTES)
    .lockTimeout(10L)     // MUTEX_LOCK 所需
    .build();
​
// 执行查询
Shop shop = cacheClient.query(CacheStrategy.LOGICAL_EXPIRE, params);

设计优势

  1. 解耦策略与参数 通过参数对象封装不同策略的差异化参数,调用者无需关注内部细节。

  2. 类型安全与可扩展性 新增策略时只需扩展枚举和参数对象,无需修改现有代码。

  3. 清晰的错误提示 每个策略在执行前校验参数,明确提示缺失的必要参数。

  4. 友好的调用接口 建造者模式让参数设置更直观,避免长参数列表的混乱。

关键问题

在 Spring 框架里,RedisClient 有一个接收 StringRedisTemplate 参数的构造函数,即便你使用 @Autowired 注入 RedisClient 时没显式传入 StringRedisTemplate,它仍能完成实例化,这得益于 Spring 的依赖注入机制。下面详细解释其实现原理和可能的情况。

原理分析

Spring 在创建 Bean 实例时,若发现该类只有一个有参构造函数,会自动查找构造函数参数类型对应的 Bean 并注入。对于 RedisClient 类,Spring 会在容器中查找 StringRedisTemplate 类型的 Bean,然后将其作为参数传入构造函数来创建 RedisClient 实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赛博猿神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值