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

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"));
    }
}
相关推荐
2501_937189238 小时前
莫凡电视:地方台专属聚合 稳定直播播放工具
android·源码·源代码管理
耶叶10 小时前
Android 新权限申请模型(Activity Result API)
android
阿拉斯攀登10 小时前
【RK3576 安卓 JNI/NDK 系列 04】JNI 核心语法(下):字符串、数组与对象操作
android·驱动开发·rk3568·瑞芯微·rk安卓驱动·jni字符串操作
2501_9159090610 小时前
不用越狱就看不到 iOS App 内部文件?使用 Keymob 查看和导出应用数据目录
android·ios·小程序·https·uni-app·iphone·webview
llxxyy卢10 小时前
web部分中等题目
android·前端
轩情吖10 小时前
MySQL之事务管理
android·后端·mysql·adb·事务·隔离性·原子性
万物得其道者成10 小时前
uni-app Android 离线打包:多环境(prod/dev)配置
android·opencv·uni-app
符哥200810 小时前
Firebase 官方提供的Quick Start-Android 库的功能集讲解
android
koeda11 小时前
android17系统兼容
android·安卓
进击的cc12 小时前
面试官:Handler 没消息时为啥不卡死?带你从源码到底层内核彻底整明白!
android·面试