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

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"));
    }
}
相关推荐
协议的旁观者2 小时前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
mmsx5 小时前
osmdroid 屏幕坐标与测量坐标互转:中心点+比例尺仿射换算
android·源码·地图·osmdroid
00后程序员张5 小时前
Android证书绑定抓包失败?Android SSL Pinning绕过实战指南
android·网络协议·计算机网络·网络安全·adb·https·ssl
杉氧5 小时前
页面栈与路由:React Navigation 与 Expo Router 深度实践
android·前端·react native
XiaoLeisj5 小时前
Jetpack Compose 知识点
android·kotlin·android jetpack·compose
又见情义6 小时前
RK3568 Android 13开机时间优化-系统应用裁剪实践
android
hunterandroid7 小时前
Android 内存泄漏排查实战:从 LeakCanary 报警到根因定位
android·前端
我命由我123458 小时前
Android 设备的日志缓冲区被写满,logcat: Unexpected EOF!
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
171320330计算机毕设编程8 小时前
基于SpringBoot的在线拍卖系统
android·java·spring boot·小程序·课程设计
用户0934077735148 小时前
HarmonyOS WPS Open SDK 实践:OpenFileRequest 打开链路与沙箱拷贝
android·typescript·harmonyos