springboot redis+hutool 实现二级缓存,降低“热点数据”对数据库的压力

简介

背景:系统有个高频的全局访问配置,该数据体积小,直接从 redis 取,怕高频访问 影响其他核心业务
例如:系统配置近乎不更改,且延迟更新允许可接受
单机:redis 查询并发,复杂类型大概3w,简单类型大概为10w+,若你的系统并发没到1w+,且没有并发要求,建议直接用redis得了,没必要改造;

这里封装了二级缓存服务方法,屏蔽了redis和本地的实现:TwoLevelCacheService

一、调用样例

1)记得编辑或删除,调用 remove 方法,
2)值获取:尽可能不要直接获取,尽量先判断是否存在该key
因为我这边实际业务是可能空列表,判断key,可避免空值一直访问数据库;若你的业务不存在空值一说,也可直接取数,然后根据值存不存在,再判断要不要访问数据库;

package com.server.common.config.service.impl;

import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.ObjectUtil;
import com.server.common.cache.service.TwoLevelCacheService;

import java.util.*;

@Slf4j
@Service
public class SysConfigServiceImpl implements SysConfigService {

    @Resource
    private TwoLevelCacheService twoLevelCacheService;

    @Override
    public SysConfigVo findById(Long id) {
        .....
    }

    @Transactional
    @Override
    public void save() {
        // 保存和、删除都得移除相应缓存
        twoLevelCacheService.remove(RedisKeyConstants.DISABLE_PROCESS_KEY);
		.....
    }

    @Override
    public Boolean getDisableStatus() {
        // 1 检查二级缓存是否存在该key (但值可能为空,所以检查key)
        String cacheKey = RedisKeyConstants.DISABLE_PROCESS_KEY;
        if (twoLevelCacheService.exists(cacheKey)) {
			// 简单类型
            return twoLevelCacheService.get(cacheKey, Boolean.class);
			/** 对象复杂类型,使用下面方法获取值
			return twoLevelCacheService.get(cacheKey, new TypeReference<>() {
            });
			**/
        }

        // 2 获取系统配置
        SysConfigVo sysConfigVo = findById(1L);
        Boolean disableStatus = ObjectUtil.isNotNull(sysConfigVo) ? sysConfigVo.getDisable() : Boolean.FALSE;

        // 3 将状态加入二级缓存
        twoLevelCacheService.set(cacheKey, disableStatus);
        return disableStatus;
    }
}

在这里插入图片描述

二 springboot 二级缓存封装的服务类

工具类的过期时间,可根据系统性质,问AI做调整

1、TwoLevelCacheService 二级缓存的服务;
2、LocalCacheManager 本地缓存的实现
3、RedisUtil 静态工具类;因为系统有些静态实现需要调用,这里没用服务的方式

2.1 TwoLevelCacheService

1)方法存在默认本地和redis缓存的过期时间,你可根据自己的业务类型做调整;
2)若你的是分布式业务,且在乎所有机器60s后本地缓存才生效,你可以使用redis广播改造remove方法;

注:若你的类型为list、map、set,你可继续新增优化方法,使用redis支持的list、map、set方法降低序列化造成的性能损耗’

package com.server.common.cache.service;

import cn.hutool.core.lang.TypeReference;
import com.server.common.cache.LocalCacheManager;
import com.server.common.util.RedisUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * 二级缓存服务(TwoLevelCacheService)
 *
 * <p>说明:
 * - 结合 LocalCacheManager(本地)和 Redis(远程)实现二级缓存。
 * - 读策略:先本地,未命中再 Redis(读透)。
 * - 写策略:同时写本地和 Redis(写同步)。
 * - 适用:小对象、热点数据;大对象请直接用 RedisUtil。
 * - 本地缓存默认 1 分钟,Redis 默认 1 小时,可覆盖。
 */
@Slf4j
@Service
public class TwoLevelCacheService {

    @Resource
    private LocalCacheManager localCacheManager;

    /**
     * 默认 Redis 过期时间 1小时
     */
    private static final long DEFAULT_REDIS_TIMEOUT = 3600;

    /**
     * 默认本地 1分钟
     */
    private static final long DEFAULT_LOCAL_TIMEOUT = TimeUnit.SECONDS.toMillis(60);

    // ================== 写入缓存 ==================

    /**
     * 写入缓存,使用默认本地和 Redis 过期时间
     *
     * @param key   缓存 key
     * @param value 缓存值
     * @param <T>   值类型
     */
    public <T> void set(String key, T value) {
        set(key, value, DEFAULT_LOCAL_TIMEOUT, DEFAULT_REDIS_TIMEOUT);
    }

    /**
     * 写入缓存,自定义本地缓存和 Redis 过期时间
     *
     * @param key                 缓存 key
     * @param value               缓存值
     * @param localTimeoutMs      本地缓存过期时间(毫秒)
     * @param redisTimeoutSeconds Redis 缓存过期时间(秒)
     * @param <T>                 值类型
     */
    public <T> void set(String key, T value, long localTimeoutMs, long redisTimeoutSeconds) {
        // 1 空值也会放入本地的
        localCacheManager.set(key, value, localTimeoutMs);
        // 2 redis 缓存不接受空值
        RedisUtil.set(key, value, redisTimeoutSeconds);
    }

    // ================== 获取缓存 ==================

    /**
     * 获取缓存(简单类型或非嵌套对象)
     *
     * @param key   缓存 key
     * @param clazz 值类型
     * @param <T>   类型
     * @return 如果存在返回缓存值,否则返回 null
     */
    public <T> T get(String key, Class<T> clazz) {
        T value = localCacheManager.get(key, clazz);
        if (value != null) {
            log.info("走本地缓存key{},val{}", key, value);
            return value;
        }

        value = RedisUtil.get(key, clazz);
        if (value != null) {
            log.info("走redis缓存key{},val{}", key, value);
            localCacheManager.set(key, value);
        }
        return value;
    }

    /**
     * 获取缓存(复杂对象 / 泛型集合)
     *
     * @param key           缓存 key
     * @param typeReference 泛型类型引用
     * @param <T>           类型
     * @return 如果存在返回缓存值,否则返回 null
     */
    public <T> T get(String key, TypeReference<T> typeReference) {
        T value = localCacheManager.get(key, typeReference);
        if (value != null) {
            log.info("走本地缓存key{},val{}", key, value);
            return value;
        }

        value = RedisUtil.get(key, typeReference);
        if (value != null) {
            log.info("走redis缓存key{},val{}", key, value);
            localCacheManager.set(key, value);
        }
        return value;
    }

    // ================== 判断、删除缓存 ==================

    /**
     * 删除缓存
     *
     * @param key 缓存 key
     */
    public boolean exists(String key) {
        return localCacheManager.contains(key) || RedisUtil.exists(key);
    }

    /**
     * 删除缓存
     *
     * @param key 缓存 key
     */
    public void remove(String key) {
        localCacheManager.remove(key);
        RedisUtil.del(key);
    }

    // ================== 过期操作 ==================

    /**
     * 刷新缓存过期时间
     *
     * <p>
     * 如果 key 存在,则同时刷新本地缓存和 Redis 缓存的 TTL。
     * </p>
     *
     * @param key                 缓存 key
     * @param localTimeoutMs      本地缓存 TTL(毫秒)
     * @param redisTimeoutSeconds Redis TTL(秒)
     */
    public void expire(String key, long localTimeoutMs, long redisTimeoutSeconds) {
        Object value = localCacheManager.get(key, Object.class);
        if (value != null) {
            localCacheManager.set(key, value, localTimeoutMs);
        }
        RedisUtil.expire(key, redisTimeoutSeconds);
    }
}

2.2 LocalCacheManager

注:1)这里为了防止强制类型转换报错,简单类型存的原值,复杂类型存的序列化的json值;
2)由于hutool 对简单类型转json,直接变成{},所以也不能直接闭眼都存储json

package com.server.common.cache;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * 本地缓存管理器(LocalCacheManager)
 *
 * <p>说明:
 * - 存储在 JVM 堆内存,用于加速热点数据访问。
 * - 适用:小对象、热点数据,读多写少。
 * - 不适用:大对象或全量列表,可能占用过多堆内存,建议直接使用 RedisUtil。
 *
 * <p>存储复杂对象统一 JSON,获取复杂对象请使用 TypeReference。
 */
@Component
public final class LocalCacheManager {

    private TimedCache<String, Object> cache;

    /**
     * 默认缓存过期时间:60 秒
     */
    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60);

    @PostConstruct
    private void init() {
        cache = CacheUtil.newTimedCache(DEFAULT_TIMEOUT_MS);
    }

    // ================== 设置缓存 ==================

    /**
     * 使用默认过期时间
     */
    public <T> void set(String key, T value) {
        set(key, value, DEFAULT_TIMEOUT_MS);
    }

    /**
     * 设置缓存
     * 简单类型直接存储,复杂对象统一 JSON
     */
    public <T> void set(String key, T value, long timeoutMs) {
        if (value == null) {
            cache.put(key, null, timeoutMs);
            return;
        }

        Object storeValue;
        if (isSimpleType(value.getClass())) {
            storeValue = value;
        } else {
            storeValue = JSONUtil.toJsonStr(value);
        }

        cache.put(key, storeValue, timeoutMs);
    }

    // ================== 获取缓存 ==================

    /**
     * 获取简单类型或非嵌套对象
     * <p>
     * 注意:仅限基本类型或简单对象,复杂对象或集合请使用 get(String, TypeReference<T>)
     */
    public <T> T get(String key, Class<T> clazz) {
        Object value = cache.get(key, false);
        if (value == null) return null;

        if (isSimpleType(clazz)) {
            return Convert.convertQuietly(clazz, value);
        } else if (value instanceof String str) {
            return JSONUtil.toBean(str, clazz, false);
        } else {
            return null;
        }
    }

    /**
     * 获取复杂对象 / 嵌套对象 / 泛型集合
     * <p>
     * 使用 TypeReference 确保嵌套泛型类型安全
     */
    public <T> T get(String key, TypeReference<T> typeReference) {
        Object value = cache.get(key, false);
        if (value == null) return null;

        if (value instanceof String str) {
            return JSONUtil.toBean(str, typeReference, false);
        } else {
            return Convert.convert(typeReference.getType(), value);
        }
    }

    // ================== 类型快捷 get ==================

    public Boolean getBool(String key) {
        return Convert.toBool(get(key, Boolean.class));
    }

    public String getString(String key) {
        return Convert.toStr(get(key, String.class));
    }

    public Integer getInt(String key) {
        return Convert.toInt(get(key, Integer.class));
    }

    public Long getLong(String key) {
        return Convert.toLong(get(key, Long.class));
    }

    public BigDecimal getBigDecimal(String key) {
        return Convert.toBigDecimal(get(key, BigDecimal.class));
    }

    // ================== 移除 / 判断 ==================

    public void remove(String key) {
        cache.remove(key);
    }

    public boolean contains(String key) {
        return cache.containsKey(key);
    }

    public void pruneAll() {
        cache.prune();
    }

    // ================== 内部方法 ==================

    private boolean isSimpleType(Class<?> clazz) {
        return clazz.isPrimitive() ||
                clazz == Boolean.class ||
                clazz == Byte.class ||
                clazz == Character.class ||
                clazz == Short.class ||
                clazz == Integer.class ||
                clazz == Long.class ||
                clazz == Float.class ||
                clazz == Double.class ||
                clazz == String.class ||
                clazz == BigDecimal.class;
    }
}

2.3 RedisUtil

当然,像list、map、set,你可以改造工具类,使用redis的原生类型,进一步提高响应性能
注:1)由于hutool 简单类型转json,直接变成{},所以也不能直接闭眼都存储json

package com.server.common.util;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;

@Slf4j
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // 静态初始化当前类
    public static RedisUtil redisUtil;

    @PostConstruct
    public void init() {
        redisUtil = this;
    }
    // ================== 存储 ==================

    /**
     * 赋值,永不过期
     *
     * @param key   key
     * @param value 值
     */
    public static void set(String key, Object value) {
        if (value == null) {
            return;
        }
        // 不设置 TTL
        Object storeValue = isSimpleType(value.getClass()) ? value : JSONUtil.toJsonStr(value);
        redisUtil.redisTemplate.opsForValue().set(key, storeValue);
    }

    /**
     * 存对象,自定义过期时间(秒)
     */
    public static <T> void set(String key, T value, long timeoutSeconds) {
        if (value == null) {
            return;
        }
        Object storeValue = isSimpleType(value.getClass()) ? value : JSONUtil.toJsonStr(value);
        redisUtil.redisTemplate.opsForValue().set(key, storeValue, Duration.ofSeconds(timeoutSeconds));
    }
    // ================== 取值(指定类型) ==================

    /**
     * 取值
     *
     * @param key 参数
     * @return 值
     */
    public static <T> T get(String key, Class<T> clazz) {
        Object value = redisUtil.redisTemplate.opsForValue().get(key);
        if (value == null) {
            return null;
        }
        if (isSimpleType(clazz)) {
            return Convert.convertQuietly(clazz, value);
        }
        if (value instanceof String str) {
            return JSONUtil.toBean(str, clazz, false);
        }
        return null;
    }

    /**
     * 获取复杂对象 / 嵌套对象 / 泛型集合
     */
    public static <T> T get(String key, TypeReference<T> typeReference) {
        Object value = redisUtil.redisTemplate.opsForValue().get(key);
        if (value == null) {
            return null;
        }
        if (value instanceof String str) {
            return JSONUtil.toBean(str, typeReference, false);
        }
        return Convert.convert(typeReference.getType(), value);
    }
    // ================== 常用类型快捷 get ==================

    public static String getString(String key) {
        return get(key, String.class);
    }

    public static Boolean getBoolean(String key) {
        return get(key, Boolean.class);
    }

    public static Integer getInteger(String key) {
        return get(key, Integer.class);
    }

    public static Long getLong(String key) {
        return get(key, Long.class);
    }

    public static BigDecimal getBigDecimal(String key) {
        return get(key, BigDecimal.class);
    }
    // ================== 判断 / 删除 ==================

    /**
     * 判断某个 key 是否存在
     *
     * @param key key
     * @return true 存在, false 不存在
     */
    public static boolean exists(String key) {
        return Boolean.TRUE.equals(redisUtil.redisTemplate.hasKey(key));
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    public static void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisUtil.redisTemplate.delete(key[0]);
            } else {
                redisUtil.redisTemplate.delete(ListUtil.of(key));
            }
        }
    }

    /**
     * 删除缓存
     *
     * @param keys 删除多个
     */
    public static void del(Collection<String> keys) {
        redisUtil.redisTemplate.delete(keys);
    }
    // ================== 过期操作 ==================

    /**
     * 设置过期时间(秒)
     */
    public static void expire(String key, long timeoutSeconds) {
        if (!exists(key)) {
            return;
        }
        redisUtil.redisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds));
    }

    /**
     * 获取值并刷新过期时间(秒)
     */
    public static <T> T getAndExpire(String key, Class<T> clazz, long timeoutSeconds) {
        T value = get(key, clazz);
        if (value != null) {
            expire(key, timeoutSeconds);
        }
        return value;
    }

    // ================== 注:基础的封装类型,不能序列化,否则 ==================
    private static boolean isSimpleType(Class<?> clazz) {
        return clazz.isPrimitive()
                || clazz == Boolean.class
                || clazz == Byte.class
                || clazz == Character.class
                || clazz == Short.class
                || clazz == Integer.class
                || clazz == Long.class
                || clazz == Float.class
                || clazz == Double.class
                || clazz == String.class
                || clazz == BigDecimal.class;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值