缓存的基础用法:解决缓存击穿,缓存穿透,缓存雪崩等问题。

java 复制代码
  //查询key是否存在缓存中?
    String value = redis.getKey("key");
    //如果缓存没有,查询数据库
    if (StringUtils.isEmpty(value)){
      value = remoteQueryDB(key);
      //放入缓存中
      redis.addKey(key,value, RandomUtils.nextInt(1000,5000));
    }
    return value;

以上是在开发中我们经常用到的缓存的写法,然而在高并发场景下会暴露缓存击穿、缓存过期时间随机但无兜底、缺乏异常处理、无并发控制 等关键问题。

下面给出解决方案:

一、核心前提:Redisson 依赖准备

首先需要引入 Redisson 依赖(以 Maven 为例),并配置 Redisson 客户端:

复制代码
<!-- Redisson 核心依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.23.3</version> <!-- 建议用最新稳定版 -->
</dependency>

二、重构后的完整代码(Redisson 实现分布式锁)

复制代码
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.Random;
import java.util.concurrent.TimeUnit;

// 基于 Redisson 的 Redis 工具类(简化版)
class RedisUtil {
    private final RedissonClient redissonClient;
    // 缓存操作的通用前缀(可选)
    private final String CACHE_PREFIX = "cache:";
    // 分布式锁前缀
    private final String LOCK_PREFIX = "lock:";

    // 初始化 Redisson 客户端(生产建议配置到Spring容器中)
    public RedisUtil() {
        Config config = new Config();
        // 单机 Redis 配置(集群/哨兵可参考Redisson官方文档)
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setPassword(null) // 无密码则设为null
              .setDatabase(0);
        this.redissonClient = Redisson.create(config);
    }

    // 查询缓存
    public String getKey(String key) {
        try {
            return redissonClient.getBucket(CACHE_PREFIX + key).get();
        } catch (Exception e) {
            // Redis 异常时返回null,降级查库
            System.err.println("Redis查询异常,key=" + key + ", error=" + e.getMessage());
            return null;
        }
    }

    // 写入缓存(带过期时间,单位:秒)
    public boolean addKey(String key, String value, int expireSeconds) {
        try {
            redissonClient.getBucket(CACHE_PREFIX + key)
                          .set(value, expireSeconds, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) {
            System.err.println("Redis写入异常,key=" + key + ", error=" + e.getMessage());
            return false;
        }
    }

    // 获取 Redisson 分布式锁(核心:可重入、自动续期)
    public RLock getLock(String key) {
        return redissonClient.getLock(LOCK_PREFIX + key);
    }
}

// 业务逻辑类(基于Redisson锁重构)
public class CacheService {
    private final RedisUtil redis = new RedisUtil();
    // 缓存过期策略:基础30分钟 + 随机0-10分钟偏移
    private final int BASE_EXPIRE_SECONDS = 30 * 60;
    private final int RANDOM_OFFSET_SECONDS = 10 * 60;
    private final Random random = new Random();
    // 锁等待时间(5秒):超过则放弃抢锁,降级查库
    private final long LOCK_WAIT_TIME = 5;
    // 锁自动释放时间(10秒):Redisson会自动续期,避免死锁
    private final long LOCK_LEASE_TIME = 10;

    public String queryData(String key) {
        // 步骤1:查询缓存,命中则直接返回
        String value = redis.getKey(key);
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }

        // 步骤2:缓存未命中,获取Redisson分布式锁
        RLock lock = redis.getLock(key);
        boolean locked = false;
        try {
            // 核心:尝试获取锁(非阻塞,最多等5秒,拿到锁后10秒自动释放)
            // Redisson 会自动续期(watch dog机制),只要线程没挂,锁不会过期
            locked = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
            
            if (locked) {
                // 双重检查:防止抢锁期间其他线程已写入缓存
                value = redis.getKey(key);
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }

                // 步骤3:锁内查库(仅一个线程执行)
                value = remoteQueryDB(key);

                // 步骤4:缓存穿透防护:数据库无值则写入空标记+短过期
                if (StringUtils.isEmpty(value)) {
                    redis.addKey(key, "", 5 * 60); // 空值缓存5分钟
                    return "";
                }

                // 步骤5:写入缓存(基础过期+随机偏移)
                int expireSeconds = BASE_EXPIRE_SECONDS + random.nextInt(RANDOM_OFFSET_SECONDS);
                redis.addKey(key, value, expireSeconds);
                return value;
            } else {
                // 步骤6:未抢到锁,自旋重试(最多3次)
                int retryCount = 0;
                while (retryCount < 3) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    value = redis.getKey(key);
                    if (StringUtils.isNotEmpty(value)) {
                        return value;
                    }
                    retryCount++;
                }
                // 重试失败,降级查库(兜底)
                return remoteQueryDB(key);
            }
        } catch (Exception e) {
            // 步骤7:异常处理,降级查库
            System.err.println("缓存逻辑异常,key=" + key + ", error=" + e.getMessage());
            return remoteQueryDB(key);
        } finally {
            // 步骤8:释放锁(必须在finally中,Redisson锁释放是幂等的)
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // 模拟远程查库(生产需加超时、重试)
    private String remoteQueryDB(String key) {
        try {
            // 模拟查库耗时
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "db_value_" + key;
    }

    // 测试方法
    public static void main(String[] args) {
        CacheService service = new CacheService();
        System.out.println(service.queryData("user_1001"));
    }
}
相关推荐
_李小白29 分钟前
【android opencv学习笔记】Day 17: 目标追踪(MeanShift)
android·opencv·学习
用户86022504674721 小时前
AI 分析头部APP系统优化框架
android
用户86022504674721 小时前
AI分析头部APP优化框架
android
2501_916007474 小时前
iOS开发中抓取HTTPS请求的完整解决方法与步骤详解
android·网络协议·ios·小程序·https·uni-app·iphone
lvronglee7 小时前
【数字图传第四步】Android App查看图传视频
android·音视频
90后的晨仔7 小时前
Android 程序入口与核心组件详解
android
90后的晨仔7 小时前
Kotlin 简介与开发环境搭建
android
BU摆烂会噶7 小时前
【LangGraph】House_Agent 实战(四):预定流程 —— 中断与人工干预
android·人工智能·python·langchain
AI玫瑰助手7 小时前
Python运算符:比较运算符(等于不等等于大于小于)与返回值
android·开发语言·python
new_dev8 小时前
Python实现Android自动化打包工具:加固、签名、多渠道一键完成
android·python·自动化