Redis缓存穿透+缓存击穿+缓存雪崩 案例及解决方案(代码实现)

2,749 阅读15分钟

0、源码地址

Redis缓存穿透+缓存击穿+缓存雪崩案例及解决方案代码地址 (github.com)

1、介绍

1.1、缓存穿透

什么是缓存穿透?

用户请求的数据在缓存和数据库中都不存在,用户每次请求数据都需要查询数据库,导致对后台数据库的频繁访问,数据库负载压力过大,这种现象就叫做缓存穿透。

产生原因

黑客大量访问不存在的key,导致数据库处理大量请求

解决方案

1、缓存空对象

2、布隆过滤器

1.2、缓存击穿

什么是缓存击穿?

redis中存在某些热点数据时,即有大量请求并发访问的key-value数据。当极热点key-value数据突然失效时,缓存未命中引起对后台数据库的频繁访问,这种现象叫缓存击穿。

产生原因

缓存上热点数据突然失效

解决方案

1、热点key设置永不过期,或者异步后台更新

2、采用互斥锁

1.3、缓存雪崩

什么是缓存雪崩?

缓存数据在同一时间点失效,导致大量的请求到数据库,从而使得是数据库崩溃。

产生原因

1、大量数据使用了同一过期时间

2、redis故障

解决方案

1、缓存数据的过期时间设置随机。防止同一时间大量缓存数据集体失效,导致数据库压力过大。

2、采用Redis高可用架构。可将热点数据分别存放到不同缓存数据库中,避免某一点由于压力过大而down掉。

3、请求限流、熔断机制、服务降级等手段。降低服务器负载。

2、缓存穿透

2.1、模拟缓存穿透

1、请求一个数据库中没有的key值,导致一直访问数据库,造成缓存穿透

PixPin_2024-07-07_20-01-40.gif

Snipaste_2024-07-07_10-58-36.png

2.2、解决方案

Snipaste_2024-07-07_10-58-36.png

2.2.1、缓存空对象

缓存穿透解决方案1-缓存空对象PixPin_2024-07-11_22-39-07.gif

image-20240712091834003.png

2.2.2、布隆过滤器

缓存穿透解决方案2-布隆过滤器PixPin_2024-07-11_22-45-43.gif

image-20240712091956038123.png

3、缓存击穿

3.1、模拟缓存击穿

1、初始化缓存,把数据库中所有数据加入缓存中

2、多线程访问热点key 88,查询是否全部命中缓存

3、删除热点key为88的缓存,模拟热点key过期,多线程访问

4、多线程访问热点key 88

PixPin_2024-07-07_20-17-56

Snipaste_2024-07-07_11-07-07

Snipaste_2024-07-07_11-13-58

3.2、解决方案

3.2.1、互斥锁

缓存击穿解决方案1-互斥锁PixPin_2024-07-11_22-49-26.gif

image-20240712092721619

3.2.2、异步定时任务

缓存击穿解决方案2-异步定时任务PixPin_2024-07-11_22-53-36.gif image-20240712092917288

4、缓存雪崩

image.png

4.1、模拟缓存雪崩

1、初始化缓存,把数据库中所有数据加入缓存中

2、多线程访问 30至40间的热点key,查询是否全部命中缓存

3、删除 30至40间的热点key的缓存,模拟多个热点key同时过期,多线程访问

4、多线程访问 30至40间的热点key

PixPin_2024-07-07_20-30-05

Snipaste_2024-07-07_11-20-24

Snipaste_2024-07-07_11-41-59

4.2、解决方案

4.2.1、互斥锁

缓存雪崩解决方案1-互斥锁PixPin_2024-07-11_22-57-26.gif

image-20240712093155767

4.2.2、异步定时任务

缓存雪崩解决方案2-异步定时任务PixPin_2024-07-11_22-59-59.gif

image-20240712095913145

4.2.3、随机过期时间

缓存雪崩解决方案3-均匀随机过期时间PixPin_2024-07-11_23-02-15.gif 缓存雪崩解决方案3-均匀随机过期时间PixPin_2024-07-11_23-02-15.gif

image-20240712100050664

5、代码实现

5.0、建表语句

CREATE TABLE `user_bank` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL COMMENT '用户Id',
  `user_name` varchar(50) DEFAULT NULL COMMENT '用户名称',
  `amount` int NOT NULL DEFAULT '0' COMMENT '用户余额',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_bank_id_uindex` (`id`),
  UNIQUE KEY `user_bank_user_id_uindex` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb3 COMMENT='用户余额表';

5.1、目录结构

image-20240712101851472

5.2、依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

<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>17.0</version>
</dependency>

5.3、配置文件

server:
  port: 8087
  servlet:
    context-path: /
# mysql连接
spring:
  datasource:
    url: jdbc:mysql://localhost:3309/redis_study?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379
    password: redis
    database: 3

5.4、controller

缓存穿透

package com.wangzhan.controller;

import com.wangzhan.domain.UserBank;
import com.wangzhan.service.CachePenetrationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author wangzhan
 * @version 1.0
 * @description 缓存穿透
 * @date 2024/7/6 11:09:30
 */
@RestController
@RequestMapping("/cachePenetration")
public class CachePenetrationController {

    @Resource
    private CachePenetrationService cachepenetrationService;

    /***
     * @description 模拟缓存穿透
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("imitateCachePenetration")
    public UserBank imitateCachePenetration(Integer id) {
        return cachepenetrationService.imitateCachePenetration(id);
    }

    /***
     * @description 解决方案一:缓存空对象
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("cacheNullKey")
    public Object cacheNullKey(Integer id) {
        return cachepenetrationService.cacheNullKey(id);
    }

    /***
     * @description 解决方案二:布隆过滤器
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("bloomFilter")
    public Object bloomFilter(Integer id) {
        return cachepenetrationService.bloomFilter(id);
    }

}

缓存击穿

package com.wangzhan.controller;

import com.wangzhan.domain.UserBank;
import com.wangzhan.service.HotspotInvalidService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author wangzhan
 * @version 1.0
 * @description 缓存击穿
 * @date 2024/7/6 11:10:17
 */
@RestController
@RequestMapping("/hotspotInvalid")
public class HotspotInvalidController {

    @Resource
    private HotspotInvalidService hotspotInvalidService;

    /***
     * @description 1、模拟缓存击穿-初始化缓存
     * @return void
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("initCache")
    public void initCache() {
        hotspotInvalidService.initCache();
    }

    /***
     * @description 2、模拟缓存击穿(数据从缓存中获取)    4、模拟缓存击穿(数据从数据库中获取)
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("imitateHotspotInvlid")
    public UserBank imitateHotspotInvlid(Integer id) {
        return hotspotInvalidService.imitateHotspotInvlid(id);
    }

    /***
     * @description 3、模拟缓存击穿-删除缓存id为88的数据
     * @return void
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("delete88IdData")
    public void delete88IdData() {
        hotspotInvalidService.delete88IdData();
    }

    /***
     * @description 解决方案一:采用互斥锁
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/9 16:50:24
     */
    @GetMapping("lock")
    public UserBank lock(Integer id) {
        return hotspotInvalidService.lock(id);
    }

    /***
     * @description 解决方案二:异步定时任务
     * @return void
     * @author wangzhan
     * @date 2024/7/9 16:50:24
     */
    @GetMapping("cornJob")
    public UserBank cornJob(Integer id) {
        return hotspotInvalidService.cornJob(id);
    }

}

缓存雪崩

package com.wangzhan.controller;

import com.wangzhan.domain.UserBank;
import com.wangzhan.service.CacheAvalancheService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author wangzhan
 * @version 1.0
 * @description 缓存雪崩
 * @date 2024/7/6 11:08:19
 */
@RestController
@RequestMapping("/cacheAvalanche")
public class CacheAvalancheController {

    @Resource
    private CacheAvalancheService cacheAvalancheService;

    /***
     * @description 1、模拟缓存击穿-初始化缓存
     * @return void
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("initCache")
    public void initCache() {
        cacheAvalancheService.initCache();
    }


    /***
     * @description 2、模拟缓存雪崩(数据从缓存中获取)    4、模拟缓存雪崩(数据从数据库中获取)
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("imitateCacheAvalanche")
    public UserBank imitateCacheAvalanche(Integer id) {
        return cacheAvalancheService.imitateCacheAvalanche(id);
    }

    /***
     * @description 3、模拟缓存雪崩-删除30-40的数据
     * @return void
     * @author wangzhan
     * @date 2024/7/3 20:33:11
     */
    @GetMapping("delete30_40IdData")
    public void delete30_40IdData() {
        cacheAvalancheService.delete30_40IdData();
    }

    /***
     * @description 解决方案一:采用互斥锁
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/10 22:28:42
     */
    @GetMapping("lock")
    public UserBank lock(Integer id) {
        return cacheAvalancheService.lock(id);
    }

    /***
     * @description 解决方案二:异步定时任务
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/10 22:29:04
     */
    @GetMapping("cornJob")
    public UserBank cornJob(Integer id) {
        return cacheAvalancheService.cornJob(id);
    }

    /***
     * @description 解决方案三:随机过期时间
     * @return int
     * @author wangzhan
     * @date 2024/7/10 22:29:04
     */
    @GetMapping("randomExpire")
    public UserBank randomExpireTime(Integer id) {
        return cacheAvalancheService.randomExpireTime(id);
    }

}

5.5、service

缓存穿透

package com.wangzhan.service;

import com.wangzhan.domain.UserBank;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:35:45
 */
public interface CachePenetrationService {

    /***
     * @description 初始化缓存
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    void initCache();

    /***
     * @description 模拟缓存穿透
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 16:59:45
     */
    UserBank imitateCachePenetration(Integer id);

    /***
     * @description 解决方案一:缓存空对象
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 16:59:45
     */
    Object cacheNullKey(Integer id);

    /***
     * @description 解决方案二:布隆过滤器
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 16:59:45
     */
    Object bloomFilter(Integer id);
}

package com.wangzhan.service.impl;

import com.alibaba.fastjson.JSON;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import com.wangzhan.service.CachePenetrationService;
import com.wangzhan.util.UserBankUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:35:55
 */
@Service
public class CachePenetrationServiceImpl implements CachePenetrationService {

    private static final Logger logger = LoggerFactory.getLogger(UserBankServiceImpl.class);

    private static final String IMITATE_KEY = "user_bank_key:wangzhan:cachePenetration:imitate"; // 缓存key
    private static final String SOLVE1_IMITATE_KEY = "user_bank_key:wangzhan:cachePenetration:solve1"; // 解决方案的缓存key
    private static final String SOLVE2_IMITATE_KEY = "user_bank_key:wangzhan:cachePenetration:solve2"; // 解决方案的缓存key

    public static final int _1W = 10000;

    //布隆过滤器里预计要插入多少数据
    public static int size = _1W;

    //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
    public static double fpp = 0.03;

    // 构建布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);

    @Resource
    private UserBankMapper userBankMapper;

    @Resource
    private UserBankUtil userBankUtil;

    private StringRedisTemplate stringRedisTemplate;

    public CachePenetrationServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /*--------------------------------缓存穿透模拟-----------------------start----------------------------*/

    @Override
    public UserBank imitateCachePenetration(Integer id) {
        return userBankUtil.queryUserBankOfHash(id, IMITATE_KEY, 1, TimeUnit.HOURS);
    }

    /*--------------------------------缓存穿透模拟-----------------------end----------------------------*/

    /*--------------------------------缓存穿透解决方案-----------------------start----------------------------*/

    @Override
    public Object cacheNullKey(Integer id) {

        String idStr = String.valueOf(id);
        UserBank userBank = userBankUtil.queryUserBankOfHash(id, SOLVE1_IMITATE_KEY, 1, TimeUnit.HOURS);

        if (userBank != null) {
            return userBank;
        }

        // 缓存和数据库都不存在
        userBank = new UserBank();
        userBank.setId(id);
        userBank.setUserName("当前用户不存在");
        userBank.setAmount(0);

        stringRedisTemplate.opsForHash().put(SOLVE1_IMITATE_KEY, idStr, JSON.toJSONString(userBank));
        stringRedisTemplate.expire(SOLVE1_IMITATE_KEY, 1, TimeUnit.HOURS);
        logger.info("缓存和数据库都不存在");
        return userBank;
    }


    @PostConstruct
    @Override
    public void initCache() {
        new Thread(() -> {
            // 查询所有数据进行缓存
            List<UserBank> userBanks = userBankMapper.list();

            userBanks.forEach(userBank -> {
                // 初始化时,先添加到布隆过滤器
                bloomFilter.put(userBank.getId());
            });

            // 通过手动删除缓存(delete30And40IdData),模拟缓存失效
            //stringRedisTemplate.expire("imitateHotspotInvlid", 1, TimeUnit.SECONDS);

        }).start();
    }
    @Override
    public Object bloomFilter(Integer id) {
        if (bloomFilter.mightContain(id)) { // 可能包含
            return userBankUtil.queryUserBankOfHash(id, SOLVE2_IMITATE_KEY, 1, TimeUnit.HOURS);
        }
        logger.info("布隆过滤器中一定不存在该数据,id为:{}", id);
        return null;
    }

    /*--------------------------------缓存穿透解决方案-----------------------end----------------------------*/

}

缓存击穿

package com.wangzhan.service;

import com.wangzhan.domain.UserBank;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:36:28
 */
public interface HotspotInvalidService {

    /***
     * @description 模拟缓存击穿
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 16:59:45
     */
    UserBank imitateHotspotInvlid(Integer id);

    /***
     * @description 删除缓存为30~40的数据
     *
     * @return void
     * @author wangzhan
     * @date 2024/7/4 08:51:08
     */
    void delete88IdData();

    /***
     * @description 初始化缓存
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    void initCache();

    /***
     * @description 解决方案一:采用互斥锁
     * @param id    主键id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/3 16:59:45
     */
    UserBank lock(Integer id);

    /***
     * @description 解决方案二:异步定时任务
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    UserBank cornJob(Integer id);

}

package com.wangzhan.service.impl;

import com.alibaba.fastjson.JSON;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import com.wangzhan.service.HotspotInvalidService;
import com.wangzhan.task.HotspotInvlidTask;
import com.wangzhan.util.UserBankUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:36:40
 */
@Service
public class HotspotInvalidServiceImpl implements HotspotInvalidService {

    private static final Logger logger = LoggerFactory.getLogger(UserBankServiceImpl.class);

    private static final String IMITATE_KEY = "user_bank_key:wangzhan:hotspotInvalid:";
    private static final String SOLVE1_IMITATE_KEY = "user_bank_key:wangzhan:hotspotInvalid:solve1:";
    private static final String SOLVE2_IMITATE_KEY = "user_bank_key:wangzhan:hotspotInvalid:solve2:";

    // 可重入锁
    private static final Lock lock = new ReentrantLock();

    @Resource
    private UserBankMapper userBankMapper;

    @Resource
    private UserBankUtil userBankUtil;

    @Resource
    private HotspotInvlidTask hotspotInvlidTask;

    private StringRedisTemplate stringRedisTemplate;

    public HotspotInvalidServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /*--------------------------------缓存击穿模拟-----------------------start----------------------------*/

    @Override
    public void initCache() {
        new Thread(() -> {
            // 查询所有数据进行缓存
            List<UserBank> userBanks = userBankMapper.list();

            userBanks.forEach(userBank -> {
                stringRedisTemplate.opsForValue().set(IMITATE_KEY + userBank.getId(), JSON.toJSONString(userBank));
                stringRedisTemplate.expire(IMITATE_KEY + userBank.getId(), 1, TimeUnit.HOURS);
            });

            // 通过手动删除缓存(delete30And40IdData),模拟缓存失效
            //stringRedisTemplate.expire("imitateHotspotInvlid", 1, TimeUnit.SECONDS);

        }).start();
    }

    @Override
    public UserBank imitateHotspotInvlid(Integer id) {
        return userBankUtil.queryUserBankOfStr(id, IMITATE_KEY + id, 1, TimeUnit.MINUTES);
    }

    @Override
    public void delete88IdData() {
        // 模拟id为88 缓存失效的情况
        stringRedisTemplate.delete(IMITATE_KEY + 88);
    }

    /*--------------------------------缓存击穿模拟-----------------------end----------------------------*/

    /*--------------------------------缓存击穿解决方案-----------------------start----------------------------*/

    @Override
    public UserBank lock(Integer id) {
        String userBankJson = stringRedisTemplate.opsForValue().get(SOLVE1_IMITATE_KEY + id);
        UserBank userBank = JSON.parseObject(userBankJson, UserBank.class);
        if (userBank != null) {
            logger.info("从【缓存】中获取数据,id为:{},结果为:{}", id, userBank);
            return userBank;
        }

        try {
            lock.lock();

            return userBankUtil.queryUserBankOfStr(id, SOLVE1_IMITATE_KEY + id, 1, TimeUnit.HOURS);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 释放锁
            lock.unlock();
        }

        return userBank;
    }


    // 初始化88的缓存,同时启动定时任务
    @PostConstruct
    public void init88IdData() {
        hotspotInvlidTask.enableScheduledUpdate();
    }

    @Override
    public UserBank cornJob(Integer id) {
        return userBankUtil.queryUserBankOfStr(id, SOLVE2_IMITATE_KEY + id, 1, TimeUnit.MINUTES);
    }

    /*--------------------------------缓存击穿解决方案-----------------------end----------------------------*/

}

缓存雪崩

package com.wangzhan.service;

import com.wangzhan.domain.UserBank;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:34:44
 */
public interface CacheAvalancheService {

    /***
     * @description 初始化缓存
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    void initCache();

    /****
     * @description 2、模拟缓存雪崩(数据从缓存中获取)    4、模拟缓存雪崩(数据从数据库中获取)
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/6 11:02:38
     */
    UserBank imitateCacheAvalanche(Integer id);

    /***
     * @description 3、模拟缓存雪崩-删除30-40的数据
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    void delete30_40IdData();

    /***
     * @description 解决方案一:采用互斥锁
     * @return void
     * @author wangzhan
     * @date 2024/7/3 17:01:01
     */
    UserBank lock(Integer id);

    /***
     * @description 解决方案二:异步定时任务
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/6 11:02:38
     */
    UserBank cornJob(Integer id);

    /***
     * @description 解决方案三:随机过期时间
     * @param id
     * @return com.wangzhan.domain.UserBank
     * @author wangzhan
     * @date 2024/7/11 09:39:47
     */
    UserBank randomExpireTime(Integer id);

}

package com.wangzhan.service.impl;

import com.alibaba.fastjson.JSON;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import com.wangzhan.service.CacheAvalancheService;
import com.wangzhan.task.CacheAvalancheTask;
import com.wangzhan.util.UserBankUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/7 10:34:57
 */
@Service
public class CacheAvalancheServiceImpl implements CacheAvalancheService {

    private static final Logger logger = LoggerFactory.getLogger(UserBankServiceImpl.class);

    private static final String IMITATE_KEY = "user_bank_key:wangzhan:cacheAvalanche:imitate:";
    // 解决方案1的key - 互斥锁
    private static final String SOLVE1_IMITATE_KEY = "user_bank_key:wangzhan:cacheAvalanche:solve1:";
    // 解决方案2的key  -  异步定时更新缓存
    private static final String SOLVE2_IMITATE_KEY = "user_bank_key:wangzhan:cacheAvalanche:solve2:";
    // 解决方案3的key  -  随机缓存失效时间
    private static final String SOLVE3_IMITATE_KEY = "user_bank_key:wangzhan:cacheAvalanche:solve3:";

    // 可重入锁
    private static final Lock lock = new ReentrantLock();

    @Resource
    private UserBankMapper userBankMapper;

    @Resource
    private UserBankUtil userBankUtil;

    @Resource
    private CacheAvalancheTask cacheAvalancheTask;

    private StringRedisTemplate stringRedisTemplate;

    public CacheAvalancheServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /*--------------------------------缓存雪崩模拟-----------------------start----------------------------*/
    @Override
    public void initCache() {
        new Thread(() -> {
            // 查询所有数据进行缓存
            List<UserBank> userBanks = userBankMapper.list();

            userBanks.forEach(userBank -> {
                stringRedisTemplate.opsForValue().set(IMITATE_KEY + userBank.getId(), JSON.toJSONString(userBank));
                stringRedisTemplate.expire(IMITATE_KEY + userBank.getId(), 1, TimeUnit.HOURS);
            });

            // 通过手动删除缓存(delete30And40IdData),模拟缓存失效
            //stringRedisTemplate.expire("imitateHotspotInvlid", 1, TimeUnit.SECONDS);

        }).start();
    }

    @Override
    public UserBank imitateCacheAvalanche(Integer id) {
        return userBankUtil.queryUserBankOfStr(id, IMITATE_KEY + id, 1, TimeUnit.HOURS);
    }

    @Override
    public void delete30_40IdData() {
        for (int i = 30; i < 40; i++) {
            stringRedisTemplate.delete(IMITATE_KEY + i);
        }
    }
    /*--------------------------------缓存雪崩模拟-----------------------end----------------------------*/

    /*--------------------------------缓存雪崩解决方案-----------------------start----------------------------*/
    @Override
    public UserBank lock(Integer id) {
        String userBankJson = stringRedisTemplate.opsForValue().get(SOLVE1_IMITATE_KEY + id);
        UserBank userBank = JSON.parseObject(userBankJson, UserBank.class);
        if (userBank != null) {
            logger.info("从【缓存】中获取数据,id为:{},结果为:{}", id, userBank);
            return userBank;
        }

        try {
            lock.lock();
            // 再次检查数据,确保在获取锁期间没有其他线程更新缓存
            userBankUtil.queryUserBankOfStr(id, SOLVE1_IMITATE_KEY + id, 1, TimeUnit.HOURS);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        return userBank;
    }

    // 初始化所有数据的缓存,同时启动定时任务
    @PostConstruct
    public void initData() {
        cacheAvalancheTask.enableScheduledUpdate();
    }

    @Override
    public UserBank cornJob(Integer id) {
        return userBankUtil.queryUserBankOfStr(id, SOLVE2_IMITATE_KEY + id, 1, TimeUnit.MINUTES);
    }

    // TODO 这里只是展示 随机过期时间的方案,并不保证线程安全。
    @Override
    public UserBank randomExpireTime(Integer id) {
        return userBankUtil.queryUserBankOfStr(id, SOLVE3_IMITATE_KEY + id, randomExpireTime(), TimeUnit.SECONDS);
    }

    // 随机生成过期时间
    public int randomExpireTime() {
        // 基本时间为60秒
        int baseExpireTime = 60;
        // 随机偏移量范围
        int offsetRange = 40;
        // 生成随机偏移量
        int randomOffset = ThreadLocalRandom.current().nextInt(-offsetRange, offsetRange + 1);

        // 返回最终的过期时间
        return baseExpireTime + randomOffset;
    }

    /*--------------------------------缓存雪崩解决方案-----------------------end----------------------------*/

}

5.6、mapper

package com.wangzhan.mapper;

import com.wangzhan.domain.UserBank;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @description user_bank
 * @author wangzhan
 * @date 2024-04-22
 */
@Mapper
//@Repository
public interface UserBankMapper {

    List<UserBank> list();

    /**
    * 新增
    * @author wangzhan
    * @date 2024/04/22
    **/
    int insert(UserBank userBank);

    /**
    * 刪除
    * @author wangzhan
    * @date 2024/04/22
    **/
    int delete(int id);

    /**
    * 更新
    * @author wangzhan
    * @date 2024/04/22
    **/
    int update(UserBank userBank);

    /**
    * 查询 根据主键 id 查询
    * @author wangzhan
    * @date 2024/04/22
    **/
    UserBank load(int id);

    /**
    * 查询 分页查询
    * @author wangzhan
    * @date 2024/04/22
    **/
    List<UserBank> pageList(int offset, int pagesize);

    /**
    * 查询 分页查询 count
    * @author wangzhan
    * @date 2024/04/22
    **/
    int pageListCount(int offset,int pagesize);

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wangzhan.mapper.UserBankMapper">

    <resultMap id="BaseResultMap" type="com.wangzhan.domain.UserBank" >
        <id column="id" property="id" />
        <result column="user_id" property="userId" />
        <result column="user_name" property="userName" />
        <result column="amount" property="amount" />
    </resultMap>

    <sql id="Base_Column_List">
        id
        ,
        user_id,
        user_name,
        amount
    </sql>

    <insert id="insert" useGeneratedKeys="true" keyColumn="id" keyProperty="id" parameterType="com.wangzhan.domain.UserBank">
        INSERT INTO user_bank
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="null != userId and '' != userId">
                user_id,
            </if>
            <if test="null != userName and '' != userName">
                user_name,
            </if>
            <if test="null != amount and '' != amount">
                amount
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="null != userId and '' != userId">
                #{userId},
            </if>
            <if test="null != userName and '' != userName">
                #{userName},
            </if>
            <if test="null != amount and '' != amount">
                #{amount}
            </if>
        </trim>
    </insert>

    <delete id="delete" >
        DELETE FROM user_bank
        WHERE id = #{id}
    </delete>

    <update id="update" parameterType="com.wangzhan.domain.UserBank">
        UPDATE user_bank
        <set>
            <if test="null != userId and '' != userId">user_id = #{userId},</if>
            <if test="null != userName and '' != userName">user_name = #{userName},</if>
            <if test="null != amount and '' != amount">amount = #{amount}</if>
        </set>
        WHERE id = #{id}
    </update>


    <select id="load" resultMap="BaseResultMap">
        SELECT <include refid="Base_Column_List" />
        FROM user_bank
        WHERE id = #{id}
    </select>

    <select id="pageList" resultMap="BaseResultMap">
        SELECT <include refid="Base_Column_List" />
        FROM user_bank
        LIMIT #{offset}, #{pageSize}
    </select>

    <select id="pageListCount" resultType="java.lang.Integer">
        SELECT count(1)
        FROM user_bank
    </select>
    <select id="list" resultType="com.wangzhan.domain.UserBank" resultMap="BaseResultMap">
        SELECT <include refid="Base_Column_List" />
        FROM user_bank
    </select>

</mapper>

5.7、异步定时任务

缓存击穿定时任务

package com.wangzhan.task;

import com.alibaba.fastjson.JSON;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * @author wangzhan
 * @version 1.0
 * @description 异步定时更新缓存
 * @date 2024/7/10 08:53:18
 */
@Component
@EnableScheduling
public class HotspotInvlidTask {

    @Resource
    private UserBankMapper userBankMapper;

    private StringRedisTemplate stringRedisTemplate;

    public HotspotInvlidTask(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private volatile boolean enableScheduledUpdate = false;

    private static final String SOLVE2_IMITATE_KEY = "user_bank_key:wangzhan:hotspotInvalid:solve2:";


    /***
     * @description 间隔55秒更新缓存
     *
     * @return void
     * @author wangzhan
     * @date 2024/7/10 11:12:51
     */
    @Scheduled(fixedDelay = 55, timeUnit = TimeUnit.SECONDS)
    public void scheduledUpdate() {
        if (!enableScheduledUpdate) {
            return;
        }

        updateDataAsync();
    }

    /**
     * 开启定时更新缓存
     */
    public void enableScheduledUpdate() {
        enableScheduledUpdate = true;
        scheduledUpdate();
    }

    /**
     * 异步更新缓存
     */
    public void updateDataAsync() {
        UserBank userBank = userBankMapper.load(88);
        stringRedisTemplate.opsForValue().set(SOLVE2_IMITATE_KEY + 88, JSON.toJSONString(userBank), 1, TimeUnit.MINUTES);
    }

}

缓存雪崩定时任务

package com.wangzhan.task;

import com.alibaba.fastjson.JSON;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author wangzhan
 * @version 1.0
 * @description 异步定时更新缓存
 * @date 2024/7/10 08:53:18
 */
@Component
@EnableScheduling
public class CacheAvalancheTask {

    @Resource
    private UserBankMapper userBankMapper;

    private StringRedisTemplate stringRedisTemplate;

    public CacheAvalancheTask(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private volatile boolean enableScheduledUpdate = false;

    private static final String SOLVE2_IMITATE_KEY = "user_bank_key:wangzhan:cacheAvalanche:solve2:";

    /***
     * @description 间隔55秒更新缓存
     *
     * @return void
     * @author wangzhan
     * @date 2024/7/10 11:12:51
     */
    @Scheduled(fixedDelay = 55, timeUnit = TimeUnit.SECONDS)
    public void scheduledUpdate() {
        if (!enableScheduledUpdate) {
            return;
        }

        updateDataAsync();
    }

    /**
     * 开启定时更新缓存
     */
    public void enableScheduledUpdate() {
        enableScheduledUpdate = true;
        scheduledUpdate();
    }

    /**
     * 异步更新缓存
     */
    public void updateDataAsync() {
        List<UserBank> list = userBankMapper.list();
        if (list.isEmpty()) {
            return;
        }
        list.forEach(userBank -> {
            stringRedisTemplate.opsForValue().set(SOLVE2_IMITATE_KEY + userBank.getId(), JSON.toJSONString(userBank), 1, TimeUnit.MINUTES);
        });
    }

}

5.8、公共方法

package com.wangzhan.util;

import com.alibaba.fastjson.JSON;
import com.wangzhan.domain.UserBank;
import com.wangzhan.mapper.UserBankMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author wangzhan
 * @version 1.0
 * @description
 * @date 2024/7/11 16:54:36
 */
@Component
public class UserBankUtil {

    private static final Logger logger = LoggerFactory.getLogger(UserBankUtil.class);

    @Resource
    private UserBankMapper userBankMapper;

    private StringRedisTemplate stringRedisTemplate;

    public UserBankUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * @param id       主键id
     * @param key      缓存key
     * @param timeout  过期时间
     * @param timeUnit 时间单位
     * @description: 查询缓存,如果缓存不存在,则查询数据库,并将数据缓存
     * @return: com.wangzhan.domain.UserBank
     * @author: wangzhan
     * @date: 2024/7/7 11:04:04
     */
    public UserBank queryUserBankOfStr(Integer id, String key, int timeout, TimeUnit timeUnit) {
        String userBankJson = stringRedisTemplate.opsForValue().get(key);
        UserBank userBank = JSON.parseObject(userBankJson, UserBank.class);

        if (userBank == null) {
            userBank = userBankMapper.load(id);
            if (userBank != null) {
                logger.info("==========设置数据到缓存=======");

                // 设置缓存
                stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(userBank), timeout, timeUnit);
            }
            logger.info("从【数据库】中获取数据,id为:{},结果为:{}", id, userBank);
            return userBank;
        }
        logger.info("从【缓存】中获取数据,id为:{},结果为:{}", id, userBank);

        return userBank;
    }

    /**
     * @param id       主键id
     * @param key      缓存key
     * @param timeout  过期时间
     * @param timeUnit 时间单位
     * @description: 查询缓存,如果缓存不存在,则查询数据库,并将数据缓存
     * @return: com.wangzhan.domain.UserBank
     * @author: wangzhan
     * @date: 2024/7/7 11:04:04
     */
    public UserBank queryUserBankOfHash(Integer id, String key, int timeout, TimeUnit timeUnit) {
        String idStr = String.valueOf(id);
        String userBankJson = (String) stringRedisTemplate.opsForHash().get(key, idStr);
        UserBank userBank = JSON.parseObject(userBankJson, UserBank.class);
        if (userBank == null) {
            // 查询数据库
            userBank = userBankMapper.load(id);
            if (userBank != null) {
                logger.info("==========设置数据到缓存=======");
                stringRedisTemplate.opsForHash().put(key, idStr, JSON.toJSONString(userBank));
                stringRedisTemplate.expire(key, timeout, timeUnit);
            }

            logger.info("从【数据库】中获取数据,id为:{},结果为:{}", idStr, userBank);
            return userBank;
        }

        logger.info("从【缓存】中获取数据,id为:{},结果为:{}", idStr, userBank);
        return userBank;
    }
}