新人笔记---redisson的简单介绍以及本人项目中的实操讲解

0 阅读16分钟

一:项目背景介绍(可以直接跳到第二节)

目前博主是自己搭建的一个智能体项目,并且自己搭建了一套记忆机制(包括全局记忆,滑动窗口实现短期记忆,长期记忆与记忆摘要,这一块具体构建在我们记忆篇笔记中会实现),博主搭建完了,但是细品一下,感觉差点味道,对了就是记忆评估制度,我们创建了一个observe表,设计多个指标,将记忆检索的情况记在表中,来评价记忆检索质量。后来博主又细品一下,缺点东西----对了,需要在前端搭建一个评测界面,来可视化我们的观测表,经过我们结合ai,熬夜爆肝终于实现了,具体的评测页面以及实现,我们都放在记忆笔记篇哪里了,或许看到这里了,大伙还不知道那里和redis有关了,博主其实想灌输的是:项目不是一下就做成的,需要先搭建框架,然后再慢慢优化,填充细节,在这里我其实是想分享一下,我们这里为什么会想到用redis优化,以及具体方案选取与落地,以及后续优化点

image.png

image.png
这里是博主具体评测界面的其中一项,他的目标是,根据我们观测表,结合数字公式计算出我们具体记忆的一些风险指数,其中我们引入AI来根据我们的评分,给出具体的判断原因以及建议,算是一个小型的基于AI的记忆风险评估报告机制,说到这里,博主想灌输各位的是,项目是先在我们目前的框架上一点一点优化的,就拿我们的这个例子,博主做完这套,发现这个机制并不友好,用户每次不小心点击评测页,他都会发送http请求来获取最新报告,但是因为报告的建议和判断指标会调用ai,实际返回响应会很慢,如果用户每次点击都这样来一套,并不友好,所以有没有一种办法来完美解决呢

说到这里,就引入我们的主题了,博主通过与ai探讨思路,通过修改多个版本建议,最后决定增加“生成实时报告功能(图片上展示了)”,用户点击,我们就发送请求生成一份最新的风险报告,这时候会比较慢,我们页面提醒用户“需要等待几分钟”来提醒用户,报告生成后返回给用户,同时存入redis,下次用户如果不需要查看最新报告,直接读取redis中的数据即可,完美解决了我们的问题

image.png

现在大致方案出来了,但是还需要考虑多线程问题,就是说如果用户多次点击或者多个用户一起点击,发送多个请求而出现的多个线程一起调用redis,导致数据错乱情况,我们项目实现了以下两个兜底策略

  • 前端用户每次点击生成最新报告,点击后用户不可再点击,直到数据返回(防止一个用户多次点击)
  • 后端考虑到我们常用的redis策略,比如逻辑过期和加锁,我们选择加锁,第一是用户本意就是获取最新数据,不能返回旧的,第二就是项目体量很小,本来就不支持很多人同时访问,减少了加锁的线程阻塞问题,综合下来我们选择加锁

那么到底怎么加锁呢,因为博主之前看的黑马点评,跟着做了自定义分布式锁,比如setnx+UUID+Lua脚本,实现下来挺麻烦的,所以我们放弃了这个方案,转向用redisson,直接复用别人已经提供的成熟的分布式锁实现。

目前完整的思路已经定下来了,现在进入下一章详细解释redisson锁以及我们项目怎么用的

二:redisson的简要讲解与使用

Redisson 是一个基于 Redis 的 Java 高级客户端与分布式框架,也是 Redis 官方推荐的 Java 客户端之一。它不仅封装了 Redis 的基础操作,更提供了一整套开箱即用的分布式数据结构与服务,极大简化了分布式系统的开发。

1、核心定位

  • 本质:Java 驻内存数据网格(In-Memory Data Grid)
  • 底层:基于高性能 Netty NIO 框架 构建,支持异步、非阻塞通信
  • 定位:将 Redis 从单纯的 K-V 缓存,升级为分布式应用的协调中枢

2、核心功能

Redisson 提供了 50 多种分布式对象和服务,最常用的包括:

(1). 分布式锁与同步器(最核心优势)
  • RLock (可重入锁) :支持可重入、公平锁、读写锁、联锁、红锁(RedLock)
  • 看门狗 (Watchdog) 机制:自动为未完成业务的锁续期(默认 30 秒),防止锁提前过期导致并发问题
  • 其他同步器RSemaphore(信号量)、RCountDownLatch(倒计时器)、RRateLimiter(限流)
(2). 分布式集合
  • 映射:RMap(支持本地缓存、过期、事件监听)
  • 列表:RList、队列:RQueueRDeque(双端队列)
  • 集合:RSetRScoredSortedSet(有序集合)
(3). 分布式对象
  • 通用对象桶 RBucket、二进制流、地理空间对象
  • 布隆过滤器 RBloomFilter:解决缓存穿透问题
  • 原子长整型 RAtomicLongRAtomicDouble
(4). 其他高级功能
  • 分布式消息队列(RTopicRQueue
  • 分布式调度任务(RScheduledExecutorService
  • 支持 Redis 所有部署模式:单机、主从、哨兵、集群

3、核心原理(以分布式锁为例)

  1. 加锁:通过 Lua 脚本 原子执行 HSET + PEXPIRE

    -- 伪代码
    if (锁不存在) then
        HSET 锁名 线程ID 1
        PEXPIRE 锁名 超时时间
    else if (当前线程持有锁) then
        HINCRBY 锁名 线程ID 1
        PEXPIRE 锁名 超时时间
    else
        返回锁剩余时间
    end
    
  2. Watchdog:后台定时任务(默认 10 秒 / 次),自动重置锁过期时间

  3. 解锁HDEL 删除线程 ID,当重入次数为 0 时删除整个锁

4、具体使用

(1)、引入依赖
<!-- SpringBoot 整合 Redisson 依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>
(2)、配置 Redis 连接
方式 1:yml 最简配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:  # 无密码留空
    database: 0

Redisson 会自动读取SpringRedis 配置,无需额外写配置类。


(3)、核心常用场景代码

直接注入 RedissonClient 即可使用

@Autowired
private RedissonClient redissonClient;

5. 分布式可重入锁(最常用)

自带看门狗自动续期,解决锁超时、死锁问题

public void business() {
    // 1. 获取锁对象
    RLock lock = redissonClient.getLock("order:lock:1001");

    try {
        // 2. 加锁(无参:一直等待,看门狗默认30s超时、10s续期)
        lock.lock();
        
        // 业务逻辑
        System.out.println("执行业务操作");

    } finally {
        // 3. 必须释放锁
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}
一、常用加锁方式区别
// 1. 阻塞等待锁,自动续期(推荐业务使用)
lock.lock();

// 2. 尝试获取锁,最多等待3秒,上锁后10秒自动过期
boolean tryLock = lock.tryLock(3, 10, TimeUnit.SECONDS);

注意,我们只可需要注意一下他的加锁方式,如果未指定锁持有时间,就走看门狗机制,自动续期锁,如果指定了,那么就会到期自动释放锁,防止死锁

二、分两种加锁方式,核心区别
1. 无参上锁(推荐,走看门狗)
// 无参,不指定过期时间
lock.lock();
  • Redisson 默认给锁设置30s 过期时间

  • 后台开启 看门狗定时任务:每 10 秒 检查一次

    • 如果当前线程还持有锁、业务没跑完 → 自动续期重置为 30s
    • 如果服务宕机 / 线程挂了 → 续期停止,30s 后锁自动释放,避免死锁
2. 带过期时间上锁(关闭看门狗)
// 手动指定 leaseTime 过期时间
lock.lock(15, TimeUnit.SECONDS);
  • 直接关闭看门狗,完全不会自动续期
  • 锁到点强制过期释放
  • 风险:业务没跑完、锁到期自动释放 → 并发安全问题

三、tryLock 同理
// 1. 不指定过期时间 → 看门狗生效
lock.tryLock();

// 2. 指定 leaseTime → 看门狗失效
lock.tryLock(long waitTime, long leaseTime, TimeUnit unit);

6. 读写锁(读多写少场景)

读读共享、读写互斥、写写互斥

RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw:lock:goods");
// 读锁
RLock readLock = rwLock.readLock();
// 写锁
RLock writeLock = rwLock.writeLock();

7、联锁 MultiLock(多把锁捆绑)

一次性锁住多个 key,要么全部加锁成功,要么全都失败,防止分段锁死锁。

获取方式
RLock lock1 = redissonClient.getLock("lock:1");
RLock lock2 = redissonClient.getLock("lock:2");
// 组合成联锁
RLock multiLock = redissonClient.getMultiLock(lock1, lock2);

8、红锁 RedLock(集群多节点加锁)

适用于 Redis 多实例部署,防止单节点故障锁失效。

获取方式

RLock lockA = redissonClient1.getLock("lock:red");
RLock lockB = redissonClient2.getLock("lock:red");
RLock lockC = redissonClient3.getLock("lock:red");

// 红锁:多数节点加锁成功才算上锁
RLock redLock = redissonClient.getRedLock(lockA, lockB, lockC);

9、统一加 / 解锁模板(所有 RLock 通用)

RLock lock = xxx; // 上面任意一种锁

try {
    lock.lock();
    // 业务代码
} finally {
    if (lock.isLocked() && lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

10.再来解释以下redisson是怎么操作redis的,有没有一种统一API操作redis

Redisson 不只是分布式锁,它能完整操作 Redis 所有数据类型,用法像 Java 集合,简单好记。只要注入:

@Autowired
private RedissonClient redissonClient;

注意我们配置redisclient时就已经注入redis的地址啥的,所以通过redisclient是可以操作我们的redis的

image.png

(1)、通用 KV 字符串(String)
// 1. 获取操作对象
RBucket<String> bucket = redissonClient.getBucket("user:name");

// 2. 存
bucket.set("张三");
// 带过期时间
bucket.set("李四", 60, TimeUnit.SECONDS);

// 3. 查
String name = bucket.get();

// 4. 删除
bucket.delete();

(2)、Hash 结构(对应 Redis Hash)
RMap<String, Object> map = redissonClient.getMap("user:1001");

// 存
map.put("age", 20);
map.put("sex", "男");

// 取
Integer age = (Integer) map.get("age");

// 原子累加
map.addAndGet("score", 5);
(3)、List 列表
RList<String> list = redissonClient.getList("msg:list");

list.add("消息1");
list.add("消息2");

// 查全部
List<String> all = list.readAll();

(4)、Set 集合
RSet<String> set = redissonClient.getSet("tag:list");
set.add("java");
set.add("redis");

// 判断是否存在
boolean has = set.contains("java");

(5)、ZSet 有序集合
RScoredSortedSet<String> zset = redissonClient.getScoredSortedSet("rank");
// 元素+分数
zset.add(99.5, "小明");
zset.add(98.0, "小红");

(6)、全局原子数字(计数器)
RAtomicLong count = redissonClient.getAtomicLong("pv:count");
count.incrementAndGet(); // +1
count.addAndGet(10);     // +10
long num = count.get();

(7)、直接执行原生 Redis 命令(兜底)

有些特殊命令 Redisson 没封装,可以原生调用:

redissonClient.getRedisCommands().set("key","val");
String val = redissonClient.getRedisCommands().get("key");

(8)、重点:和 SpringRedisTemplate 区别
  1. RedisTemplate

    • 贴近 Redis 原生命令
    • 序列化要自己配、容易乱码
    • 只有基础缓存能力,没有分布式锁 / 限流 / 队列
  2. Redisson

    • Java 对象 / 集合方式操作 Redis

    • 自带序列化、开箱即用

    • 自带:分布式锁、读写锁、红锁、限流、队列、延时队列

    • 企业级微服务首选

介绍到这里,基本基础是介绍完了,就可用它来实现redis分布式锁了,但是在那之前,博主还需要解释一个点,也是博主的困惑点,RLock lock = redissonClient.getLock(lockKey);我们调用它,他就会在我们的redis中添加锁标识吗

核心答案
1. 只执行这一行代码:
RLock lock = redissonClient.getLock(lockKey);

不会往 Redis 存任何数据 不会加锁、无任何网络请求、无 Redis 写入

分步拆解全过程

第一步:只是「创建锁对象」
// 仅仅是本地Java创建一个RLock对象,单纯内存对象
RLock lock = redissonClient.getLock("lock:order");
  • 只在 JVM 本地生成对象
  • 不连 Redis、不发命令、不写 key
第二步:才是真正加锁、写入 Redis
lock.lock(); 
// 或
lock.tryLock();

执行这行才会发 Lua 脚本到 Redis

  1. 在 Redis 中创建 Hash 类型的 key
  2. key = 你传入的 lockKey
  3. hash field = 当前线程唯一 ID
  4. hash value = 重入次数
  5. 同时设置过期时间(无参就是默认 30s,开启看门狗)

你去 Redis 里就能查到这个 key,就是锁标识。


看 Redis 里锁真实结构

假设锁 key:lock:orderRedis 中实际存储:

key:lock:order
type:hash
field:uuid:threadId (唯一线程标识)
value:重入次数(123...可重入)

解锁的时
lock.unlock();
  • 执行 Lua 脚本
  • 重入次数 -1
  • 次数减到 0 → 删除整个 lockKey
  • Redis 锁标识消失

image.png

image.png

这是博主为了验证时截的图,我们这里调用redisson的lock方法,确实会加锁,维护一个Hash结构,这里的value表示的是锁重入次数,底层是通过state变量来计数的,符合我们项目中加的是可重复锁这个需求

三、具体项目中的使用流程

┌─────────────────────────────────────────────────────────────────┐
│  generateReportWithCache(userId, agentId, range, scope, refresh) │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 步骤1: 参数标准化和Key构建                                        │
│ - normalizedRange: 标准化时间范围                                │
│ - scopeKey/scopeLabel: 范围标识                                  │
│ - reportCacheKey: 缓存Key (memory:evaluation:report:{hash})      │
│ - lockKey: 分布式锁Key (memory:evaluation:lock:{hash})           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 步骤2: 首次缓存检查                                               │
│ IF !refresh THEN                                                │
│   读取缓存 → 命中? → 直接返回缓存报告                             │
│ END IF                                                          │
└─────────────────────────────────────────────────────────────────┘
                              │
                    缓存未命中 ─┘
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 步骤3: 获取分布式锁 (Redisson)                                    │
│ lock.tryLock(1ms, 15min)                                        │
│ - 等待1毫秒获取锁                                                │
│ - 锁租约15分钟(防止死锁)                                        │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
        获取锁成功                        获取锁失败
              │                               │
              ▼                               ▼
┌─────────────────────┐           ┌─────────────────────────────────┐
│ 继续步骤5           │           │ 步骤4: 等待其他线程生成完成        │
└─────────────────────┘           │ waitForCachedReport(2分钟)       │
                                  │ - 轮询检查缓存是否就绪             │
                                  │ - 命中? → 返回缓存报告             │
                                  │ - 超时? → 抛出异常                 │
                                  └─────────────────────────────────┘
                                              │
                                    等待成功 ─┘

┌─────────────────────────────────────────────────────────────────┐
│ 步骤5: 双重检查缓存                                               │
│ IF !refresh THEN                                                │
│   再次读取缓存 → 命中? → 返回缓存报告                             │
│ END IF                                                          │
└─────────────────────────────────────────────────────────────────┘
                              │
                    缓存仍不存在 ─┘
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 步骤6: 生成报告并写入缓存                                         │
│ 1. markProcessing(): 标记生成状态                                │
│ 2. buildReport(): 生成评测报告(耗时操作,可能调用AI)              │
│ 3. cacheReport(): 写入Redis缓存,设置TTL                          │
│ 4. 返回报告                                                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 步骤7: 清理和释放锁 (finally块)                                   │
│ 1. clearProcessing(): 清除生成状态标记                            │
│ 2. lock.unlock(): 释放分布式锁                                   │
└─────────────────────────────────────────────────────────────────┘  

这里博主就偷懒以下的,让ai生成的我们项目的流程图,因为博主实在懒的画流程图了,现在我来讲解一下其中的关键点

(1)步骤5的双重检查

获取有人会问,为什么需要双重检查,别的线程生成报告并且写入缓存,释放锁后,其他线程直接读取缓存不就行了吗

这里其实会有突发情况,我们项目是用户选择是否读取最新报告数据,如果用户选择读取缓存,但是缓存过期了,那就应该去生成最新报告数据并加入缓存,这里有个漏洞,线程1判断缓存为空,先判断锁是否被占有,判断锁未被占有后,获取锁成功,会去生成报告,写入缓存,线程2这时来了,他判断缓存为空(此时线程1缓存还未写完毕),尝试获取锁并获取成功(线程1刚好写入缓存并释放锁),好了,如果没有双重检查,线程1会再去生成一份报告,并写入缓存,而双重检查就是让线程2在获取锁时再去检查一遍缓存中有没有报告,防止报告重复生成

线程A: 获取锁成功 ──→ 双重检查 ──→ 未命中 ──→ 生成报告 ──→ 写入缓存
                              ↑
线程B: 获取锁失败 ──→ 等待 ──────────────────────┘
                              ↑
线程C: 获取锁失败 ──→ 等待 ──────────────────────┘
                              ↓
线程B被唤醒: 获取锁成功 ──→ 双重检查 ──→ 【命中!直接返回】✅

(2)步骤4的轮询检查
还是拿上面线程1的例子,我们线程1持有锁的期间,其他线程就无法获取锁,按道理说会阻塞等待,但是我们这里面不是让他们一直获取锁,而是让他们去轮询检查缓存是否写入数据了,比如每个1s检查一次,并且设置最多轮询时间,比如最多10秒,也就是该线程最多轮询检查10次,如果均没有获取到缓存数据,那么就直接放弃,返回null,而不是一直阻塞,提醒用户“过一会再尝试”

线程A: 获取锁成功 ──→ 正在生成报告(耗时10秒)──→ 写入缓存
                              ↑
线程B: 获取锁失败 ──→ 进入 waitForCachedReport
                         ↓
                    每隔1秒轮询缓存 ──→ 等待线程A完成 ──→ 读取新缓存

(3)锁租期机制
就是引入了我们的redisson锁,设置了过期时间,避免长时间持有锁造成线程堵塞与死锁

说到这里,其实我们核心点就已经做完了,对于我们本身就不是高并发体系,并不需要做的很复杂,做成这样就够我们项目使用了,避免堆积其他高并发的技术栈,导致项目冗余

这里我们附上项目源码,仅供参考,这只是一部分

public MemoryEvaluationReportDTO generateReportWithCache(String userId,
                                                        String agentId,
                                                        String range,
                                                        String scope,
                                                        boolean refresh) {
    // ========== 步骤1:参数标准化和Key构建 ==========
    String normalizedRange = normalizeRange(range);
    String scopeKey = normalizeScopeKey(scope, agentId);
    String scopeLabel = buildScopeLabel(scope, agentId);
    // 构建报告缓存Key:memory:evaluation:report:{hash}
    String reportCacheKey = buildReportCacheKey(userId, agentId, normalizedRange, scopeKey);
    // 构建分布式锁Key:memory:evaluation:lock:{hash}
    String lockKey = buildLockKey(reportCacheKey);

    // ========== 步骤2:首次缓存检查(从缓存中尝试读取报告,如果用户传递刷新,那么就调用ai生成报告,再加入缓存)==========
    if (!refresh) {//非强制刷新模式下,优先读取缓存
        MemoryEvaluationReportDTO cachedReport = readCachedReport(reportCacheKey);
        if (cachedReport != null) {
            log.info("[MemoryEvaluation] cache hit userId={}, agentId={}, range={}, scopeKey={}",
                    userId, agentId, normalizedRange, scopeKey);
            return cachedReport;  // 缓存命中,直接返回
        }
    }

    // ========== 步骤3:获取分布式锁 ==========
    RLock lock = redissonClient.getLock(lockKey);
    boolean locked = false;
    try {
        // 尝试获取锁,等待1毫秒,锁租约15分钟,超过1秒未获取到锁,抛出异常,如果持有锁,最多持有15分钟,当时间自动释放锁
        locked = lock.tryLock(1, LOCK_LEASE_MILLIS, TimeUnit.MILLISECONDS);
        
        // ========== 步骤4:获取锁失败,说明其他线程正在生成报告,等待其他线程生成完成 ==========
        if (!locked) {
            // 等待缓存生成完成,最多等待2分钟
            MemoryEvaluationReportDTO waitingReport = waitForCachedReport(reportCacheKey, CACHE_WAIT_MILLIS);
            if (waitingReport != null) {
                log.info("[MemoryEvaluation] cache ready after wait userId={}, agentId={}, range={}, scopeKey={}",
                        userId, agentId, normalizedRange, scopeKey);
                return waitingReport;  // 等待成功,返回缓存数据
            }
            // 等待超时,抛出异常
            throw new IllegalStateException("评测报告正在生成中,请稍后重试");
        }

        // ========== 步骤5:获取锁成功,双重检查缓存 ==========
        if (!refresh) {
            MemoryEvaluationReportDTO cachedReport = readCachedReport(reportCacheKey);
            if (cachedReport != null) {
                log.info("[MemoryEvaluation] cache hit after lock userId={}, agentId={}, range={}, scopeKey={}",
                        userId, agentId, normalizedRange, scopeKey);
                return cachedReport;  // 其他线程已生成,直接返回
            }
        }

        // ========== 步骤6:生成报告并写入缓存 ==========
        // 标记生成状态,用于其他线程等待时检查
        markProcessing(reportCacheKey, userId, agentId, normalizedRange, scopeKey);
        // 构建评测报告(耗时操作,可能涉及AI调用)
        MemoryEvaluationReportDTO report = buildReport(userId, agentId, normalizedRange, scopeLabel, refresh);
        // 写入缓存,设置TTL
        cacheReport(reportCacheKey, report, cacheTtl(normalizedRange));
        log.info("[MemoryEvaluation] report generated and cached userId={}, agentId={}, range={}, scopeKey={}",
                userId, agentId, normalizedRange, scopeKey);
        return report;
        
    } catch (InterruptedException e) {
        // 线程被中断,恢复中断状态并抛出异常
        Thread.currentThread().interrupt();
        throw new IllegalStateException("评测报告生成被中断", e);
    } finally {
        // ========== 步骤7:清理和释放锁 ==========
        clearProcessing(reportCacheKey);  // 清除生成状态标记
        if (locked && lock.isHeldByCurrentThread()) {
            lock.unlock();  // 释放分布式锁
        }
    }
}

最后附上我们的redis存储的报告表的结构图,以助于大家理解

image.png

image.png

这只是一部分,整个的评测数据太多了,下面展示一下前端页面

image.png

image.png

这也只是部分展览图,详细看后续的记忆篇

四、后续的优化功能

当前我们只是实现了报告存入redis,但是有个问题是不好溯源,不好查询历史评测记录,做人工判断,统计啥的,所以后续需要增加一个历史评测表,放在Mysql,通过字段拆分,构建索引,避免将我们整个redis的内容塞在mysql,然后做好数据库与redis的主从一致性问题,比如常见的延迟双删,先更新数据库再删除缓存,加redisson读写锁,异步mq通知等等,后续有待优化