一、概述

 1.缓存穿透(Cache Penetration)
 * 缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。
 * 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。
 * 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。
 * 攻击方式:
 * 攻击者使用不存在的用户id频繁请求;
 * 这些请求都会直接访问数据库,导致数据库压力过大;
 * 解决缓存穿透的常见方案:
 * (1)缓存空值:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;
 * (2)布隆过滤器(Bloom Filter):再缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;
 * (3)互斥锁(Mutex Lock):缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
 * (4)接口层校验:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)
 * (5)热点数据永不过期:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
 * (6)缓存预热:在系统启动或低峰期,预先加载热点数据到缓存中。
 * (7)实时监控与限流:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
2.缓存击穿(Cache Breakdown)
 * 缓存击穿的定义:缓存击穿(Cache Breakdown)是指某个热点key(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。
 * 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。
 * 核心问题:单个热点key失效后,大量请求同时访问数据库。
 * 3.缓存雪崩(Cache Avalanche)
 * 问题描述:
 * 大量缓存key在同一时间过期;
 * 大量请求直接访问数据库,导致数据库崩溃;

二、代码

2.1 controller层

package com.study.sredis.stept001.controller;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.RateLimiter;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.service.UserService;
import com.study.sredis.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 1.缓存穿透(Cache Penetration)
 * 缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。
 * 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。
 * 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。
 * 攻击方式:
 * 攻击者使用不存在的用户id频繁请求;
 * 这些请求都会直接访问数据库,导致数据库压力过大;
 * 解决缓存穿透的常见方案:
 * (1)缓存空值:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;
 * (2)布隆过滤器(Bloom Filter):再缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;
 * (3)互斥锁(Mutex Lock):缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
 * (4)接口层校验:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)
 * (5)热点数据永不过期:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
 * (6)缓存预热:在系统启动或低峰期,预先加载热点数据到缓存中。
 * (7)实时监控与限流:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
 * <p>
 * 2.缓存击穿(Cache Breakdown)
 * 缓存击穿的定义:缓存击穿(Cache Breakdown)是指某个热点key(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。
 * 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。
 * 核心问题:单个热点key失效后,大量请求同时访问数据库。
 * 3.缓存雪崩(Cache Avalanche)
 * 问题描述:
 * 大量缓存key在同一时间过期;
 * 大量请求直接访问数据库,导致数据库崩溃;
 */
@RestController
@RequestMapping("/userRedis")
public class CacheTestController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private UserService userService;
    //    缓存key前缀
    private static final String KEY_HOT = "hot:user:";
    //    热点ID列表——实际可放到配置中心或DB
    private static final List<String> HOT_ID_LIST = Arrays.asList("1001", "1002", "1003");
    //    缓存前缀
    private static final String KEY_HOT_PRE = "user:cache:hot:";
    /* ========== 1. 令牌桶:10 QPS,突发 20 ========== */
    private final RateLimiter rateLimiter = RateLimiter.create(10.0);

    /* ========== 2. 异常计数 key 前缀 ========== */
    private static final String KEY_NULL_COUNT = "user:null:count:";
    private static final String KEY_BLOCK_FLAG  = "user:block:";   // 阻塞标记
    private static final int  MAX_NULL_THRESHOLD = 30;            // 1 分钟内最多 30 次 null
    private static final int  BLOCK_SECONDS      = 60;            // 触发后封 60 秒

    @PostMapping("/selectById")
    public R selectById(@RequestBody User user) {
        User userInfo = userService.getById(user.getId());
        stringRedisTemplate.opsForValue().set(String.valueOf(userInfo.getId()), userInfo.getUserName());
        String value = stringRedisTemplate.opsForValue().get(String.valueOf(userInfo.getId()));
        HashMap<String, User> map = new HashMap<>();
        map.put(value, userInfo);
        return R.ok(map);
    }

    /**
     * 解决方案一:缓存空值
     * 原理:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;
     * 优点:简单易实现,直接拦截后续相同请求;
     * 缺点:1)内存浪费(存储大量无效null值)
     * 2)可能出现短时不一致,如:数据已补录,但缓存未及时失效(如需强一致性,可以在更新数据时,删除/覆盖缓存)
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByIdNull")
    public R selectByIdNull(@RequestBody User user) {
//        1.从redis查询用户信息缓存;
        String key = String.valueOf(user.getId());
        String userJson = stringRedisTemplate.opsForValue().get(key);
//        2.判断是否存在;
        if (StrUtil.isNotBlank(userJson)) {
            //3.存在,直接返回;
            User user1 = JSONUtil.toBean(userJson, User.class);
            return R.ok(user1);
        }
//        判断命中的是否是空值
        if (userJson != null) {
//            返回错误信息,解决缓存穿透问题
            return R.fail("用户信息不存在");
        }
//        4.不存在,根据id查询数据库;
        User userInfo = userService.getById(user.getId());
        if (userInfo == null) {
            //5.如果数据库也不存在,将空字符串写入redis,设置过期事件,解决缓存穿透问题;
            stringRedisTemplate.opsForValue().set(key, "", 1000, TimeUnit.MINUTES);
//            返回错误信息,解决缓存穿透问题
            return R.fail("用户信息不存在");
        }
//        6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(userInfo), 1000, TimeUnit.MINUTES);
        return R.ok(userInfo);
    }

    /**
     * 解决方案二:布隆过滤器(Bloom Filter)
     * 原理:在缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;
     * 优点:内存占用低(没有多余的Key),适合海量数据,查询时间复杂度O(1);
     * 缺点:1)存在错误的可能(可能将不存在判断未存在);
     * 2)实现复杂,删除元素困难(需重建过滤器);
     * 适用场景:数据量大且允许误判(如黑名单校验);
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByIdBloomFilter")
    public R selectByIdBloomFilter(@RequestBody User user) {
        if (user.getId() == null || user.getId() <= 0) {
            return R.fail("用户id不合适");
        }
        long startTime = System.currentTimeMillis();
        User userInfo = userService.selectByIdBloomFilter(user);
        long endTime = System.currentTimeMillis();
        HashMap<String, Object> result = new HashMap<>();
        result.put("time", endTime - startTime);
        if (userInfo != null) {
            result.put("info", userInfo);
            return R.ok(result);
        } else {
            result.put("info", null);
            return R.fail(result);
        }
    }

    /**
     * 直接查询用户信息——与使用布隆过滤器做对比
     *
     * @param user
     * @return
     */
    @PostMapping("/comWithSelectByIdBloomFilter")
    public R comWithSelectByIdBloomFilter(@RequestBody User user) {
        if (user.getId() == null || user.getId() <= 0) {
            return R.fail("用户id不合法");
        }
        long startTime = System.currentTimeMillis();
        User userInfo = userService.selectByIdDirect(user);
        long endTime = System.currentTimeMillis();
        HashMap<String, Object> result = new HashMap<>();
        result.put("time", endTime - startTime);
        if (userInfo != null) {
            result.put("info", userInfo);
            return R.ok(result);
        } else {
            result.put("info", null);
            return R.fail(result);
        }
    }

    /**
     * 解决方案三:互斥锁
     * 原理:缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
     * 优点:避免大量请求同时穿透到数据库。
     * 缺点:1)分布式环境下需使用分布式锁(如 Redis RedLock)。
     * 2)锁竞争可能成为性能瓶颈。
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByWithLock")
    public R selectByWithLock(@RequestBody User user) {
        if (user.getId() == null || user == null) {
            return R.fail("用户id不能为空");
        }
        long startTime = System.currentTimeMillis();
        HashMap<String, Object> result = new HashMap<>();
        try {
//            User userInfo = userService.selectByIdWithLock(user);        //使用Redisson分布式锁
//            User userInfo = userService.selectByIdWithSpringLock(user);  //使用Spring的Redis分布式锁
            User userInfo = userService.selectByIdWithSimpleLock(user);    //使用RedisTemplate实现简单分布式锁
            long endTime = System.currentTimeMillis();
            if (result != null) {
                result.put("time", endTime - startTime);
                result.put("info", userInfo);
                return R.ok(result);
            } else {
                result.put("time", endTime - startTime);
                result.put("info", null);
                return R.fail(result);
            }
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            result.put("time", endTime - startTime);
            result.put("info", null);
            return R.fail(result);
        }
    }

    /**
     * 解决方案四;接口层校验
     * 原理:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)。
     * 优点:低成本防御恶意攻击(如扫描全表 ID)。
     * 缺点:无法拦截合法参数但实际不存在的数据请求。
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByIdWithInterfacter")
    public R selectByIdWithInterfacter(@RequestBody User user) {
//        1.接口层校验:必须是纯数字且正整数
        Long id = validId(String.valueOf(user.getId()));
        if (id == null) {
            return R.fail("非法id格式");
        }
//        2.业务查询
        User userInfo = userService.getById(user.getId());
        return user == null ? R.fail("用户不存在") : R.ok(userInfo);
    }

    /**
     * 校验 ID 合法性:纯数字、正整数、非空
     * 合法返回 Long,非法返回 null
     * 作用:用于接口层校验
     */
    private Long validId(String idRaw) {
        if (idRaw == null || idRaw.isEmpty()) {
            return null;
        }
        try {
            long id = Long.parseLong(idRaw.trim());
            return id > 0L ? id : null;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * 解决方法五:热点数据永不过期
     * 原理:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
     * 优点:彻底避免缓存失效导致的穿透。
     * 缺点:数据一致性依赖更新机制,需处理脏数据问题。
     * 实现:结合定时任务或事件驱动更新缓存。
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByIdWithHot")
    public R<User> selectByIdWithHot(@RequestBody User user) {
        String key = KEY_HOT + user.getId();
        String lockKey = key + ":lock";
        String lockVal = UUID.fastUUID().toString();

        int retry = 3;                       // 最多自旋 3 次
        while (retry-- > 0) {
            /* 1. 缓存命中立即返回 */
            String json = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(json)) {
                return R.ok(JSON.parseObject(json, User.class));
            }
            /* 2. 抢锁(5 s 自动过期) */
            Boolean locked = stringRedisTemplate.opsForValue()
                    .setIfAbsent(lockKey, lockVal, 5, TimeUnit.SECONDS);
            if (Boolean.TRUE.equals(locked)) {
                try {
                    /* 3. double check */
                    json = stringRedisTemplate.opsForValue().get(key);
                    if (StrUtil.isNotBlank(json)) {
                        return R.ok(JSON.parseObject(json, User.class));
                    }
                    /* 4. 回源 DB */
                    User userDB = userService.getById(user.getId());
                    if (userDB != null) {
                        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(userDB));
                    }
                    return userDB == null ? R.fail("用户信息不存在") : R.ok(userDB);
                } finally {
                    /* 5. 原子释放锁 */
                    String lua =
                            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                    "  return redis.call('del', KEYS[1]) " +
                                    "else return 0 end";
                    stringRedisTemplate.execute(
                            new DefaultRedisScript<>(lua, Long.class),
                            Collections.singletonList(lockKey),
                            lockVal);
                }
            }
            /* 6. 没抢到锁,自旋 100 ms 继续重试 */
            ThreadUtil.sleep(100);
        }
        /* 7. 重试耗尽 */
        return R.fail("系统繁忙,请稍后再试");
    }
    /* ===================== 后台定时更新 ===================== */

    /**
     * 每 30 秒刷新一次热点缓存,保证“永不过期”且数据最终一致
     */
    @Scheduled(fixedDelay = 30_000)
    public void refreshHotCache() {
        for (String id : HOT_ID_LIST) {
            String key = KEY_HOT + id;
            User user = userService.getById(id);
            if (user != null) {
                // 直接覆盖旧值,仍不带过期时间
                stringRedisTemplate.opsForValue().set(key, user.toString());
            } else {
                // DB 已删除,同步删缓存
                stringRedisTemplate.delete(key);
            }
        }
    }

    /**
     * 解决方案六:缓存预热
     * 原理:在系统启动或低峰期,预先加载热点数据到缓存中。
     * 优点:减少冷启动时的缓存穿透风险。
     * 缺点:需提前知道热点数据(可通过历史日志分析)。
     *
     * @param user
     * @return
     */
    @PostMapping("/selectByIdWithPreHot")
    public R selectByIdWithPreHot(@RequestBody User user) {
        String key = KEY_HOT_PRE + user.getId();
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(json)){
            return R.ok(JSON.parseObject(json,User.class));
        }
        return R.fail("用户信息不存在");
    }

    /**
     * 解决方案七:实时监控与限流
     * 原理:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
     * 优点:兜底防御,避免突发攻击。
     * 缺点:需配套监控和告警系统。
     *
     * @param user
     * @return
     */
    @PostMapping("/selectById4")
    public R selectById4(@RequestBody User user) {

        /* 3. 先拿令牌,拿不到直接限流 */
        if (!rateLimiter.tryAcquire()) {
            return R.fail("系统繁忙,请稍后再试");
        }

        /* 4. 是否处于封禁期 */
        String blockKey = KEY_BLOCK_FLAG + user.getId();
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(blockKey))) {
            return R.fail("请求过于频繁,请稍后再试");
        }

        /* 5. 查库 */
        User userInfo = userService.getById(user.getId());

        /* 6. 异常计数(null 视为异常) */
        if (userInfo == null) {
            String countKey = KEY_NULL_COUNT + user.getId();
            long count = stringRedisTemplate.opsForValue().increment(countKey);
            stringRedisTemplate.expire(countKey, 60, TimeUnit.SECONDS);          // 1 分钟窗口
            if (count >= MAX_NULL_THRESHOLD) {
                stringRedisTemplate.opsForValue().set(blockKey, "1", BLOCK_SECONDS, TimeUnit.SECONDS);
                return R.fail("触发保护策略,稍后再试");
            }
            return R.fail("用户信息不存在");
        }
        /* 7. 正常返回 */
        return R.ok(userInfo);
    }
}

2.2 service层

2.2.1 service接口

package com.study.sredis.stept001.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.study.sredis.stept001.domain.User;

public interface UserService extends IService<User> {
    /**
     * 使用布隆过滤器查询用户信息
     *
     * @param user
     * @return
     */
    User selectByIdBloomFilter(User user);

    /**
     * 直接查询用户信息——不使用布隆过滤器
     *
     * @param user
     * @return
     */
    User selectByIdDirect(User user);

    /**
     * 根据id查询用户 —— 使用Redisson分布式锁(推荐!!!)
     *
     * @param user
     * @return
     */
    public User selectByIdWithLock(User user);

    /**
     * 根据id查询用户 —— 使用Spring的Redis分布式锁
     *
     * @param user
     * @return
     */
    public User selectByIdWithSpringLock(User user);

    /**
     * 根据id查询用户 —— 使用RedisTemplate实现简单分布式锁
     *
     * @param user
     * @return
     */
    public User selectByIdWithSimpleLock(User user);
}

2.2.2 serviceImpl

package com.study.sredis.stept001.service.impl;

import cn.hutool.bloomfilter.BloomFilter;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.mapper.userMapper;
import com.study.sredis.stept001.service.UserService;
import com.study.sredis.utils.RedisLockUtil;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

@Service
public class UserServiceImpl extends ServiceImpl<userMapper, User> implements UserService {
    @Autowired
    private BloomFilter bloomFilter;
    @Autowired
    private userMapper userMapper;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private RedisLockUtil redisLockUtil;
    private static final String CACHE_PREFIX = "user:";
    private static final String LOCK_PREFIX = "lock:user";
    private static final long CACHE_EXPIRE = 300;  //5分钟

    /**
     * 使用布隆过滤器查询用户信息
     *
     * @param user
     * @return
     */
    @Override
    public User selectByIdBloomFilter(User user) {
//        1.先检查布隆过滤器;
        if (!bloomFilter.contains("user:" + user.getId())) {
            return null;
        }
//        2.布隆过滤器通过,查询数据库;
        User userInfo = userMapper.selectById(user.getId());
        return userInfo;
    }

    /**
     * 直接查询用户信息
     *
     * @param user
     * @return
     */
    @Override
    public User selectByIdDirect(User user) {
        User userInfo = userMapper.selectById(user.getId());
        return userInfo;
    }

    /**
     * 根据id查询用户 - 使用Redisson分布式锁(推荐!!!)
     *
     * @param user
     * @return
     */
    @Override
    public User selectByIdWithLock(User user) {
        String cacheKey = CACHE_PREFIX + user.getId();
        String lockKey = LOCK_PREFIX + user.getId();
//        1.先查缓存
        User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
        if (userInfo != null) {
            return "NULL".equals(userInfo) ? null : userInfo;
        }
//        2.获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
//            尝试获取锁:等待时间5秒,锁过期时间是30秒
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (locked) {
//                3.双重检查缓存
                userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
                if (userInfo != null) {
                    return "NULL".equals(userInfo) ? null : userInfo;
                }
//                4.查询数据库
                userInfo = userMapper.selectById(user.getId());
//                5.回填缓存
                if (userInfo != null) {
                    redisTemplate.opsForValue().set(cacheKey, userInfo, CACHE_EXPIRE, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS);  //短时间缓存空值
                }
                return userInfo;

            } else {
//                6.获取锁失败,等待并重试
                Thread.sleep(100);  //短暂等待
                return selectByIdWithLock(user); //递归重试
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        } finally {
//            7.释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 根据id查询用户 —— 使用Spring的Redis分布式锁
     *
     * @param user
     * @return
     */
    @Override
    public User selectByIdWithSpringLock(User user) {
        String cacheKey = CACHE_PREFIX + user.getId();
        String lockKey = LOCK_PREFIX + user.getId();
//        1.先查缓存
        User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
        if (userInfo != null) {
            return "NULL".equals(userInfo) ? null : userInfo;
        }
//        2.使用Spring的分布式锁
        RedisLockRegistry lockRegistry = new RedisLockRegistry(redisTemplate.getConnectionFactory(), "user-lock");
        Lock lock = lockRegistry.obtain(lockKey);
        try {
//            尝试获取锁
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
//                    3.双重检查缓存
                    userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
                    if (userInfo != null) {
                        return "NULL".equals(userInfo) ? null : userInfo;
                    }
//                    4.查询数据库
                    userInfo = userMapper.selectById(user.getId());
//                    5.回填缓存
                    if (userInfo != null) {
                        redisTemplate.opsForValue().set(cacheKey, user, Duration.ofMinutes(5));
                    } else {
                        redisTemplate.opsForValue().set(cacheKey, "NULL", Duration.ofMinutes(1));
                    }
                    return userInfo;
                } finally {
                    lock.unlock();
                }
            } else {
//                获取锁失败,递归重试
                Thread.sleep(100);
                return selectByIdWithLock(user);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        }
    }

    /**
     * 根据id查询用户 —— 使用RedisTemplate实现简单分布式锁
     *
     * @param user
     * @return
     */
    @Override
    public User selectByIdWithSimpleLock(User user) {
        String cacheKey = "user:" + user.getId();
        String lockKey = "lock:user:" + user.getId();
        String lockValue = UUID.randomUUID().toString();
//        1.先查缓存
        User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
        if (userInfo != null) {
            return "NULL".equals(userInfo) ? null : userInfo;
        }
        boolean locked = false;
        try {
//            2.尝试获取锁
            for (int i = 0; i < 3; i++) {
                locked = redisLockUtil.tryLock(lockKey, lockValue, 30, TimeUnit.SECONDS);
                if (locked) break;
                Thread.sleep(100);
            }
            if (locked) {
//                3.双重检查缓存
                userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
                if (userInfo != null) {
                    return "NULL".equals(userInfo) ? null : userInfo;
                }
//                4.查询数据库
                userInfo = userMapper.selectById(user.getId());
//                5.回填缓存
                if (userInfo != null) {
                    redisTemplate.opsForValue().set(cacheKey, userInfo, Duration.ofMinutes(5));
                } else {
                    redisTemplate.opsForValue().set(cacheKey, "NULL", Duration.ofMinutes(1));
                }
                return userInfo;
            } else {
//                获取锁失败,直接查询数据(降级)
                return userMapper.selectById(user.getId());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        } finally {
            if (locked) {
                redisLockUtil.releaseLock(lockKey, lockValue);
            }
        }
    }

    /**
     * 初始化布隆过滤器
     */
    @PostConstruct
    public void initBloomFilter() {
//        1.查询所有用户信息
        List<User> userList = userMapper.selectList(null);
        for (User user : userList) {
            bloomFilter.add("user:" + user.getId());
        }
    }
}

2.3 依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 集成redis依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <!--        hutool依赖-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>
        <!-- 分布式锁 + Redis 客户端一站式 -->
        <!-- 分布式锁 starter:redisson-spring-boot-starter -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.3</version>
        </dependency>
        <!-- 或者使用Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        <!--Spring分布式锁-->
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-redis</artifactId>
        </dependency>
        <!--用于json类型转换-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <!-- 若用 Guava 令牌桶 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.3-jre</version>
        </dependency>
    </dependencies>

2.4 工具类

2.4.1 布隆过滤器配置类——BloomFilterConfig

package com.study.sredis.utils;

import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.bloomfilter.BloomFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置类
 */
@Configuration
public class BloomFilterConfig {
    /**
     * 创建布隆过滤器
     * 参数说明:预计插入数量,误判率
     */
    @Bean
    public BloomFilter bloomFilter() {
        // 预计插入1000个元素,误判率0.01
        return new BitMapBloomFilter(1000);
    }
}

2.4.2 缓存预热配置类——CacheWarmRunner

package com.study.sredis.utils;


import com.alibaba.fastjson.JSON;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * 缓存预热配置类
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheWarmRunner implements ApplicationRunner {

    private final StringRedisTemplate redisTpl;
    private final UserService userService;

    private static final String KEY_HOT = "user:cache:hot:";   // 与 Controller 保持一致
    private static final List<Long> HOT_ID_LIST = Arrays.asList(1001L, 1002L, 1003L);

    @Override
    public void run(ApplicationArguments args) {
        log.info("====== 缓存预热开始 ======");
        for (Long id : HOT_ID_LIST) {
            String key = KEY_HOT + id;
            if (Boolean.TRUE.equals(redisTpl.hasKey(key))) continue;
            User user = userService.getById(id);
            if (user != null) {
                redisTpl.opsForValue().set(key, JSON.toJSONString(user));
                log.info("已预热 -> {}", key);
            }
        }
        log.info("====== 缓存预热结束 ======");
    }
}

2.4.3 redis配置类——RedisConfig

package com.study.sredis.utils;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
//    redisson锁
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://localhost:6379")
                .setDatabase(0);
        return Redisson.create(config);
    }
}

2.4.4 锁配置类——RedisLockUtil

package com.study.sredis.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 锁配置类
 */
@Component
public class RedisLockUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 尝试获取锁
     */
    public boolean tryLock(String key, String value, long expire, TimeUnit timeUnit) {
        return Boolean.TRUE.equals(
                redisTemplate.opsForValue().setIfAbsent(key, value, expire, timeUnit)
        );
    }

    /**
     * 释放锁
     */
    public boolean releaseLock(String key, String value) {
        String currentValue = redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            redisTemplate.delete(key);
            return true;
        }
        return false;
    }
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐