@@ -0,0 +1,643 @@
# ThumbUP 技术调研文档
!!! note "💡 一句话概述"
基于 thumb-up 项目源码,逐项解析五个核心技术点的实现状态、实现链路、方案选型依据、现存漏洞及完善方向。原调研中识别的未实现项(布隆过滤器、空值短缓存、互斥锁防击穿、分布式锁)已全部实现并更新。
---
## 🔑 核心概念
1. **布隆过滤器 + 空值短缓存 ** — 三重防护机制拦截缓存穿透与击穿
2. **Caffeine + Redis 二级缓存 ** — HeavyKeeper 热 Key 探测,仅热 Key 提升至 L1
3. **Lua 脚本 + 时间片分桶 ** — 原子操作保证一致性,10 秒分桶暂存增量
4. **HeavyKeeper Top-K ** — 概率衰减机制自适应探测热 Key,优于 CMS
5. **Redis 分布式锁 ** — SETNX + Lua 脚本释放,替代原 synchronized + intern() 方案
---
## 📝 目录
1. [缓存穿透与缓存击穿防护 ](#1-缓存穿透与缓存击穿防护 )
2. [Caffeine + Redis 二级缓存 ](#2-caffeine--redis-二级缓存 )
3. [Lua 脚本原子性 + 时间片分桶 + 定时落库 + 补偿兜底 ](#3-lua-脚本原子性--时间片分桶--定时落库--补偿兜底 )
4. [HeavyKeeper Top-K 探测 vs CMS ](#4-heavykeeper-top-k-探测-vs-cms )
5. [单机锁与分布式锁 ](#5-单机锁与分布式锁 )
---
## 1. 缓存穿透与缓存击穿防护
### 1.1 实现状态:✅ 已实现
三重防护机制已全部实现:
| 防护机制 | 实现文件 | 状态 |
|----------|----------|------|
| **布隆过滤器 ** | BloomFilterManager.java | ✅ |
| **空值短缓存 ** | CacheManager.java | ✅ |
| **互斥锁防击穿 ** | CacheManager.java | ✅ |
### 1.2 实现链路
#### 1.2.1 布隆过滤器
**选型 ** : Guava `BloomFilter` ,预期插入量 100万,误判率 1%
- **初始化**: `@PostConstruct` 启动时创建
- **写入时机**: `ThumbServiceImpl.doThumb()` 点赞成功后调用 `bloomFilterManager.add(userId, blogId)`
- **判断时机**: `ThumbServiceImpl.hasThumb()` 查询前调用 `bloomFilterManager.mightContain(userId, blogId)`
``` java
@Component
public class BloomFilterManager {
private BloomFilter < String > thumbBloomFilter ;
@PostConstruct
public void init ( ) {
thumbBloomFilter = BloomFilter . create (
Funnels . stringFunnel ( StandardCharsets . UTF_8 ) ,
1_000_000L , // 预期插入量
0 . 01 // 误判率 1%
) ;
}
public void add ( Long userId , Long blogId ) {
thumbBloomFilter . put ( userId + " : " + blogId ) ;
}
public boolean mightContain ( Long userId , Long blogId ) {
return thumbBloomFilter . mightContain ( userId + " : " + blogId ) ;
}
}
```
**拦截流程 ** :
``` mermaid
flowchart TD
A["hasThumb(userId, blogId)"] --> B{"bloomFilterManager\n.mightContain()"}
B -->|"false(一定不存在)"| C["直接返回 false,不查 Redis"]
B -->|"true(可能存在)"| D["继续查询二级缓存"]
```
#### 1.2.2 空值短缓存
使用独立的 Caffeine 实例(`nullValueCache` ),TTL 30秒,最大 10000 条目:
- 当 Redis 查询返回 null 时,写入空值标记到 `nullValueCache`
- 后续请求命中空值短缓存直接返回 null,30秒后自动过期
``` java
private final Cache < String , Object > nullValueCache = Caffeine . newBuilder ( )
. maximumSize ( 10000 )
. expireAfterWrite ( 30 , TimeUnit . SECONDS ) // 30秒短缓存
. build ( ) ;
public Object get ( String hashKey , String key ) {
// ... 1. 查本地缓存(L1)
// 2. 查空值短缓存(防缓存穿透)
Object nullMarker = nullValueCache . getIfPresent ( compositeKey ) ;
if ( nullMarker ! = null ) {
return null ; // 空值短缓存命中,避免穿透到 Redis
}
// ... 3. 加互斥锁 → 4. 查 Redis
if ( redisValue = = null ) {
nullValueCache . put ( compositeKey , NULL_PLACEHOLDER ) ; // 写入空值短缓存
return null ;
}
// ...
}
```
#### 1.2.3 互斥锁防缓存击穿
使用 `ConcurrentHashMap<String, Object>` 管理锁对象(避免 `intern()` 内存泄漏):
- 锁粒度为 `compositeKey` ( hashKey:key),确保同一 Key 只有一个线程回源
- 采用 double-check 模式:加锁后再次检查本地缓存
``` java
private final ConcurrentHashMap < String , Object > lockMap = new ConcurrentHashMap < > ( ) ;
public Object get ( String hashKey , String key ) {
// 1. 查本地缓存
// 2. 查空值短缓存
// 3. 加互斥锁,防止缓存击穿
Object lock = lockMap . computeIfAbsent ( compositeKey , k - > new Object ( ) ) ;
synchronized ( lock ) {
// double-check:再次检查本地缓存
value = localCache . getIfPresent ( compositeKey ) ;
if ( value ! = null ) return value ;
// 查 Redis...
// 热 Key 提升到本地缓存...
}
}
```
### 1.3 方案选型依据
| 维度 | 说明 |
|------|------|
| **为什么用 Guava BloomFilter ** | Java 生态最成熟的布隆过滤器实现,API 简洁,性能优秀 |
| **为什么空值缓存用独立实例 ** | 与正常缓存隔离,避免空值污染正常缓存的淘汰策略 |
| **为什么空值 TTL 30秒 ** | 平衡穿透防护效果与数据一致性——太短则防护效果差,太长则数据变更后延迟过大 |
| **为什么用 ConcurrentHashMap 管理锁 ** | 避免 `String.intern()` 内存泄漏,支持锁对象的复用和清理 |
### 1.4 现存漏洞
| 漏洞 | 说明 |
|------|------|
| **缓存雪崩 ** | 所有本地缓存 Key 的 TTL 均为固定 5 分钟,存在同时过期的风险(可考虑加随机偏移) |
| **lockMap 无限膨胀 ** | `lockMap` 中的锁对象不会自动清理,长期运行后可能占用较多内存 |
| **布隆过滤器假阳性 ** | 1% 误判率意味着少量不存在的 Key 会穿透到 Redis,但不会漏判真正的热 Key |
---
## 2. Caffeine + Redis 二级缓存
### 2.1 实现状态:✅ 已实现
### 2.2 实现链路
**架构概览 ** :
``` mermaid
flowchart TD
A["请求 → CacheManager.get()"] --> B{"L1: Caffeine\n本地缓存命中?"}
B -->|"命中"| C["直接返回"]
B -->|"未命中"| D{"L2: Redis Hash\n命中?"}
D -->|"未命中"| E["返回 null"]
D -->|"命中"| F["HeavyKeeper\n记录访问频率"]
F --> G{"进入 Top-100\n且 count ≥ 10?"}
G -->|"是"| H["提升到 L1"]
G -->|"否"| I["返回 Redis 值"]
H --> I
```
**L1 本地缓存配置 ** :
``` java
Caffeine . newBuilder ( )
. maximumSize ( 1000 ) // 最大 1000 个条目
. expireAfterWrite ( 5 , TimeUnit . MINUTES ) // 写入后 5 分钟过期
. build ( ) ;
```
**二级查询逻辑 ** ( `get` 方法):
1. 先查 L1(Caffeine)→ 命中则记录访问并返回
2. L1 未命中 → 查 L2( Redis Hash)
3. Redis 也未命中 → 返回 null
4. Redis 命中 → 通过 HeavyKeeper 记录访问频率
5. 若 Key 被判定为热 Key(进入 Top-100 且访问次数 ≥ 10)→ 提升到 L1
``` java
public Object get ( String hashKey , String key ) {
String compositeKey = this . buildCacheKey ( hashKey , key ) ;
// 1. 查本地缓存
Object value = localCache . getIfPresent ( compositeKey ) ;
if ( value ! = null ) {
hotKeyDetector . add ( key , 1 ) ; // 记录访问
return value ;
}
// 2. 查 Redis
Object redisValue = redisTemplate . opsForHash ( ) . get ( hashKey , key ) ;
if ( redisValue = = null ) {
return null ;
}
// 3. 记录访问次数,判断是否热 Key
AddResult addResult = hotKeyDetector . add ( key , 1 ) ;
// 4. 热 Key 提升到本地缓存
if ( addResult . isHotKey ( ) ) {
localCache . put ( compositeKey , redisValue ) ;
}
return redisValue ;
}
```
**写一致性 ** ( `putIfPresent` 方法):
``` java
public void putIfPresent ( String hashKey , String key , Object value ) {
String compositeKey = this . buildCacheKey ( hashKey , key ) ;
Object object = localCache . getIfPresent ( compositeKey ) ;
if ( object = = null ) return ; // 仅更新已存在的 Key,避免冷数据污染
localCache . put ( compositeKey , value ) ;
}
```
**热 Key 淘汰 ** :
``` java
@Scheduled ( fixedRate = 20 , timeUnit = TimeUnit . SECONDS )
public void cleanHotKeys ( ) {
hotKeyDetector . fading ( ) ; // 每 20 秒对所有计数器右移一位(减半)
}
```
### 2.3 方案选型依据
| 维度 | 说明 |
|------|------|
| **为什么用 Caffeine ** | Java 生态最优的本地缓存库,Window TinyLfu 淘汰策略命中率高于 LRU,性能优于 Guava Cache |
| **为什么不用 Spring @Cacheable ** | 项目需要精细化控制(热 Key 判断、条件性提升),`@Cacheable` 注解方式不够灵活 |
| **为什么用 HeavyKeeper 而非固定阈值 ** | 固定阈值无法适应流量波动;HeavyKeeper 基于概率衰减,能自适应调整热 Key 集合 |
| **为什么 putIfPresent 而非 put ** | 避免冷数据污染本地缓存——只有已经被提升到 L1 的 Key 才会被更新 |
### 2.4 现存漏洞
| 漏洞 | 说明 |
|------|------|
| **L1/L2 数据不一致 ** | `ThumbServiceImpl.undoThumb()` 中先删 Redis 再更新 L1,若中间进程崩溃,L1 仍持有旧值 |
| **本地缓存容量固定 ** | `maximumSize(1000)` 在高并发场景下可能不足,应根据实际 QPS 动态调整 |
| **HeavyKeeper 线程安全 ** | `add()` 方法中 `total += increment` 非原子操作,高并发下存在竞态条件 |
| **fading 期间锁竞争 ** | `fading()` 遍历 50万个 Bucket 并逐个加锁,可能造成短暂的性能抖动 |
### 2.5 完善方向
- 为 `total` 字段改用 `AtomicLong` 或在 `fading()` 中统一加锁
- 考虑引入 `refreshAfterWrite` 替代 `expireAfterWrite` ,实现异步刷新而非同步淘汰
- 在 `CacheManager` 中增加缓存命中率监控指标(Micrometer/Prometheus)
---
## 3. Lua 脚本原子性 + 时间片分桶 + 定时落库 + 补偿兜底
### 3.1 实现状态:✅ 已实现
### 3.2 实现链路
**整体架构 ** :
``` mermaid
flowchart TD
A["用户点赞"] --> B["Lua 脚本原子操作\nRedis 临时分桶\ntemp_thumb:HH:mm:SS"]
B -->|"每10秒"| C["SyncThumb2DBJob\n批量落库 → MySQL"]
C -->|"每天凌晨2点"| D["CompensatoryJob\n补偿兜底"]
```
#### 3.2.1 Lua 脚本保证原子性
**点赞脚本(THUMB_SCRIPT) ** :
``` lua
-- KEYS[1] = temp_thumb:{timeSlice} -- 临时计数键
-- KEYS[2] = thumb:{userId} -- 用户点赞状态键
-- ARGV[1] = userId
-- ARGV[2] = blogId
-- 1. 检查是否已点赞
if redis.call ( ' HEXISTS ' , userThumbKey , blogId ) == 1 then
return - 1 -- 已点赞
end
-- 2. 获取旧值
local oldNumber = tonumber ( redis.call ( ' HGET ' , tempThumbKey , hashKey ) or 0 )
-- 3. 原子更新:临时计数 + 用户状态
redis.call ( ' HSET ' , tempThumbKey , hashKey , oldNumber + 1 )
redis.call ( ' HSET ' , userThumbKey , blogId , 1 )
return 1
```
**取消点赞脚本(UNTHUMB_SCRIPT) ** :逻辑对称,先检查存在性,再递减计数并删除状态标记。
**原子性保证 ** :Redis 单线程执行 Lua 脚本期间不会被其他命令打断,确保「检查 + 计数更新 + 状态标记」三步操作的原子性。
#### 3.2.2 10 秒时间片分桶
``` java
private String getTimeSlice ( ) {
DateTime nowDate = DateUtil . date ( ) ;
return DateUtil . format ( nowDate , " HH:mm: " + ( DateUtil . second ( nowDate ) / 10 ) * 10 ) ;
}
```
- 将时间划分为 10 秒粒度的桶:`14:35:00` 、`14:35:10` 、`14:35:20` ...
- 每个桶对应一个 Redis Hash Key: `temp_thumb:14:35:10`
- Hash 中的 field 为 `userId:blogId` , value 为 `1` (点赞)或 `-1` (取消点赞)
- **设计意图**:避免高频写入直接打 MySQL,通过分桶聚合增量
#### 3.2.3 定时任务批量落库
``` java
@Scheduled ( fixedRate = 10000 ) // 每 10 秒执行
@Transactional ( rollbackFor = Exception . class )
public void run ( ) {
// 计算上一个 10 秒时间窗口(避免读取正在写入的桶)
int second = ( DateUtil . second ( nowDate ) / 10 - 1 ) * 10 ;
// ... 读取 temp_thumb:{date} 全部数据
// 批量插入点赞记录
thumbService . saveBatch ( thumbList ) ;
// 批量删除取消点赞记录
thumbService . remove ( wrapper ) ;
// 批量更新博客点赞计数
blogMapper . batchUpdateThumbCount ( blogThumbCountMap ) ;
// 异步删除临时 Key
Thread . startVirtualThread ( ( ) - > redisTemplate . delete ( tempThumbKey ) ) ;
}
```
**关键设计 ** :
- 处理**上一个**时间窗口的数据,避免与当前写入冲突
- 使用 `CASE WHEN` 批量更新博客计数(单条 SQL)
- 使用 Java 21 虚拟线程异步删除已处理的临时 Key
#### 3.2.4 每日补偿任务
``` java
@Scheduled ( cron = " 0 0 2 * * * " ) // 每天凌晨 2 点
public void run ( ) {
// 扫描所有 temp_thumb:* Key
Set < String > thumbKeys = redisTemplate . keys ( RedisKeyUtil . getTempThumbKey ( " " ) + " * " ) ;
// 对每个残留 Key 重新执行同步
for ( String date : needHandleDataSet ) {
syncThumb2DBJob . syncThumb2DBByDate ( date ) ;
}
}
```
### 3.3 方案选型依据
| 维度 | 说明 |
|------|------|
| **为什么用 Lua 而非 Redis 事务 ** | MULTI/EXEC 不支持条件判断(如 HEXISTS 检查),Lua 脚本可以在 Redis 服务端完成「检查+写入」的原子操作 |
| **为什么 10 秒分桶 ** | 平衡实时性与批量效率——太短则桶过多、同步频繁;太长则用户取消点赞后状态延迟过大 |
| **为什么处理上一个窗口 ** | 当前窗口仍在写入中,读取会丢失数据;上一个窗口已关闭,数据完整 |
| **为什么需要补偿任务 ** | 常规定时任务可能因进程重启、异常等原因遗漏某些桶;每日补偿作为最终兜底 |
### 3.4 现存漏洞
| 漏洞 | 说明 |
|------|------|
| **Lua 脚本 value 语义问题 ** | 取消点赞时 value 为 `-1` ,但 `SyncThumb2DBJob` 中 `thumbType == 0` 时跳过,若同一用户在同一桶内先赞后取消,value 变为 `0` ,该操作会丢失 |
| **批量删除未使用索引 ** | `LambdaQueryWrapper` 构建的 OR 条件在数据量大时可能导致全表扫描 |
| **补偿任务使用 KEYS 命令 ** | `redisTemplate.keys()` 在生产环境会阻塞 Redis(O(N) 扫描),应改用 `SCAN` |
| **异步删除无重试 ** | `Thread.startVirtualThread()` 删除临时 Key 失败后无重试机制,残留 Key 依赖补偿任务清理 |
| **事务范围过大 ** | `SyncThumb2DBJob.run()` 上的 `@Transactional` 包裹了整个方法,若批量数据量大,事务持锁时间过长 |
### 3.5 完善方向
- **修复 value 语义**:将 Lua 脚本的 value 改为独立的点赞/取消标记(如 `INCR=1` / `DECR=-1` 分开存储),或在同步时处理 value=0 的情况
- **KEYS → SCAN**:补偿任务改用 `SCAN` 命令分批扫描,避免阻塞 Redis
- **异步删除重试**:引入重试机制或使用 Redis Key 的 TTL 自动过期作为兜底
- **分页批量**: `SyncThumb2DBJob` 应限制单次处理的数据量,避免大事务
---
## 4. HeavyKeeper Top-K 探测 vs CMS
### 4.1 实现状态:✅ 已实现(HeavyKeeper),❌ CMS 未实现
### 4.2 实现链路
**算法核心 ** :
``` mermaid
flowchart TD
A["输入 Key"] --> B["MurmurHash3 计算指纹"]
B --> C["遍历 depth 行 Bucket"]
C --> D{"Bucket 状态"}
D -->|"空桶"| E["写入指纹和计数"]
D -->|"指纹匹配"| F["计数递增"]
D -->|"指纹冲突"| G["按衰减概率递减现有计数"]
E --> H{"计数 ≥ minCount\n且进入 Top-K?"}
F --> H
G --> H
H -->|"是"| I["标记为热 Key"]
H -->|"否"| J["继续"]
```
**核心参数 ** :
| 参数 | 值 | 含义 |
|------|-----|------|
| k | 100 | 追踪 Top-100 热 Key |
| width | 100000 | 每行 Bucket 数量(Sketch 宽度) |
| depth | 5 | 行数(哈希函数个数) |
| decay | 0.92 | 衰减系数 |
| minCount | 10 | 最小出现次数才记录 |
**衰减机制 ** (两层):
1. **写入时衰减 ** :指纹冲突时,按 `decay^count` 的概率递减现有计数
``` java
double decay = bucket . count < LOOKUP_TABLE_SIZE ?
lookupTable [ bucket . count ] : // 预计算的 0.92^i
lookupTable [ LOOKUP_TABLE_SIZE - 1 ] ;
if ( random . nextDouble ( ) < decay ) {
bucket . count - - ;
}
```
2. **定时衰减 ** ( `fading()` ):每 20 秒所有计数器右移一位(减半)
``` java
public void fading ( ) {
for ( Bucket [ ] row : buckets ) {
for ( Bucket bucket : row ) {
synchronized ( bucket ) {
bucket . count = bucket . count > > 1 ; // 减半
}
}
}
// minHeap 中的计数也减半
}
```
**热 Key 判定流程 ** :
``` java
AddResult addResult = hotKeyDetector . add ( key , 1 ) ;
if ( addResult . isHotKey ( ) ) { // Key 进入 Top-100 且 count ≥ 10
localCache . put ( compositeKey , redisValue ) ; // 提升到本地缓存
}
```
### 4.3 HeavyKeeper vs CMS 对比分析
| 维度 | CMS( Count-Min Sketch) | HeavyKeeper |
|------|------------------------|-------------|
| **数据结构 ** | 二维计数数组 + 多个哈希函数 | 二维 Bucket 数组 + 指纹 + 衰减概率 |
| **冲突处理 ** | 计数累加(所有 Key 共享计数) | 按衰减概率递减冷 Key 计数 |
| **冷 Key 影响 ** | 冷 Key 累加会污染热 Key 计数 | 冷 Key 被概率衰减淘汰,不影响热 Key |
| **高频更新准确率 ** | 高频 Key 的计数被其他 Key 稀释 | 高频 Key 持续递增,低频 Key 被衰减 |
| **适用场景 ** | 频率估计(不需要精确 Top-K) | Top-K 热 Key 探测(需要精确排序) |
| **内存开销 ** | 仅存计数 | 存指纹 + 计数(略高) |
| **时间适应性 ** | 无内置衰减,需手动重置 | 内置衰减机制,自动适应流量变化 |
**选型理由 ** :点赞系统的核心需求是「识别热 Key 并提升到本地缓存」,HeavyKeeper 的概率衰减机制天然适配流量变化——热点博客的 Key 持续被访问不会被衰减淘汰,而长尾 Key 会自然衰减到阈值以下。CMS 没有内置衰减机制,冷 Key 的累积计数会稀释热 Key 的准确性。
### 4.4 现存漏洞
| 漏洞 | 说明 |
|------|------|
| **total 非线程安全 ** | `total += increment` 非原子操作,并发下存在竞态 |
| **minHeap 线程安全 ** | `add()` 方法中先遍历 `minHeap.stream()` 再操作,两步之间可能被其他线程修改 |
| **衰减精度损失 ** | `fading()` 使用右移(`>> 1` )而非浮点除法,多次衰减后计数会快速归零 |
| **指纹碰撞 ** | MurmurHash3 的 32 位指纹在 Key 量大时碰撞概率上升,可能导致冷 Key 被误判为热 Key |
| **minHeap.stream() 性能 ** | 每次 `add()` 都遍历 minHeap 查找已有 Key( O(k)),k=100 时影响不大,但扩展性差 |
### 4.5 完善方向
- 将 `total` 改为 `AtomicLong`
- 对 `minHeap` 的查找改用 `HashMap<String, Node>` 辅助索引,将查找复杂度从 O(k) 降为 O(1)
- 考虑使用 64 位指纹降低碰撞概率
- 为 `fading()` 引入可配置的衰减因子(而非固定右移),避免过度衰减
---
## 5. 单机锁与分布式锁
### 5.1 实现状态:✅ 已实现(分布式锁)
| 场景 | 状态 | 说明 |
|------|------|------|
| 单机锁 | ✅ 已实现(旧方案) | `synchronized` + `String.intern()` (已弃用) |
| 分布式锁 | ✅ 已实现(新方案) | 基于 Redis SETNX + Lua 脚本的分布式锁 |
> **注**:原 `synchronized` + `intern()` 方案已替换为 Redis 分布式锁,解决了内存泄漏问题,同时支持多实例部署。
### 5.2 分布式锁实现链路
**核心设计 ** :
``` mermaid
flowchart TD
subgraph 加锁流程
A["tryLock(lockKey)"] --> B["SETNX lock:thumb:{lockKey}\n{uuid:threadId} PX 10000"]
B -->|"成功"| C["存储锁标识到 ThreadLocal"]
B -->|"失败"| D["重试(50ms间隔,3秒超时)"]
D --> B
end
subgraph 释放流程
E["unlock(lockKey)"] --> F["Lua 脚本:验证锁标识 → 删除锁\n(原子操作)"]
F --> G["清理 ThreadLocal"]
end
```
**加锁实现 ** :
``` java
public boolean tryLock ( String lockKey , long acquireTimeout , long lockTimeout ) {
String fullLockKey = buildLockKey ( lockKey ) ;
String lockValue = generateLockValue ( ) ; // UUID + threadId
while ( true ) {
// 原子性 SETNX
Boolean result = stringRedisTemplate . opsForValue ( )
. setIfAbsent ( fullLockKey , lockValue , lockTimeout , TimeUnit . MILLISECONDS ) ;
if ( Boolean . TRUE . equals ( result ) ) {
LOCK_VALUE_HOLDER . set ( lockValue ) ; // 存储到 ThreadLocal
return true ;
}
// 超时检查 + 重试
if ( System . currentTimeMillis ( ) - startTime > = acquireTimeout ) {
return false ;
}
Thread . sleep ( 50L ) ; // 50ms 重试间隔
}
}
```
**释放锁实现 ** ( Lua 脚本保证原子性):
``` java
private static final DefaultRedisScript < Long > UNLOCK_SCRIPT = new DefaultRedisScript < > ( """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""" , Long . class ) ;
public boolean unlock ( String lockKey ) {
String lockValue = LOCK_VALUE_HOLDER . get ( ) ;
// Lua 脚本:验证锁标识 → 删除锁(只释放自己持有的锁)
Long result = stringRedisTemplate . execute ( UNLOCK_SCRIPT ,
Collections . singletonList ( fullLockKey ) , lockValue ) ;
LOCK_VALUE_HOLDER . remove ( ) ; // 清理 ThreadLocal
return Long . valueOf ( 1L ) . equals ( result ) ;
}
```
**业务使用 ** ( `ThumbServiceImpl` ):
``` java
public Boolean doThumb ( DoThumbRequest doThumbRequest , HttpServletRequest request ) {
// ...
String lockKey = " USERID: " + userId ;
return redisLockUtil . executeWithLock ( lockKey , ( ) - > {
return transactionTemplate . execute ( status - > {
// ... 检查是否已点赞 → 更新计数 → 写入 Redis + 布隆过滤器
} ) ;
} ) ;
}
```
### 5.3 方案选型依据
| 维度 | 说明 |
|------|------|
| **为什么用 Redis SETNX 而非 synchronized ** | `synchronized` + `intern()` 存在内存泄漏,且不支持多实例部署;Redis 分布式锁天然支持跨 JVM 互斥 |
| **为什么用 Lua 脚本释放锁 ** | 避免误释放其他线程的锁——必须验证锁标识一致才能删除 |
| **为什么用 ThreadLocal 存储锁标识 ** | 确保每个线程只能释放自己持有的锁,避免并发下的误操作 |
| **为什么用 UUID + threadId 作为锁标识 ** | UUID 保证全局唯一,threadId 便于调试和日志追踪 |
| **为什么对用户加锁而非对博客加锁 ** | 对博客加锁会导致同一博客的所有点赞请求串行,严重影响并发性能;对用户加锁只阻塞同一用户的并发操作 |
### 5.4 现存漏洞
| 漏洞 | 说明 |
|------|------|
| **锁续期问题 ** | 当前锁过期时间固定 10 秒,若业务执行时间超过 10 秒,锁会自动过期,可能导致并发问题 |
| **重试无退避 ** | 固定 50ms 重试间隔,在高并发场景下可能造成大量 Redis 请求(可考虑指数退避) |
| **锁不可重入 ** | 当前实现不支持同一线程多次获取同一把锁(会死锁),但点赞场景不需要可重入 |
| **Redis 单点故障 ** | 若 Redis 宕机,分布式锁完全失效;可考虑 RedLock 算法(多 Redis 实例)提升可靠性 |
---
## 📊 总结
| 技术点 | 状态 | 实现方式 | 核心风险 |
|--------|------|----------|----------|
| 布隆过滤器 | ✅ 已实现 | Guava BloomFilter( 100万容量,1%误判率) | 假阳性导致少量穿透 |
| 空值短缓存 | ✅ 已实现 | 独立 Caffeine 实例(30秒 TTL) | 空值缓存与实际数据变更的短暂不一致 |
| 互斥锁防缓存击穿 | ✅ 已实现 | ConcurrentHashMap + synchronized + double-check | lockMap 无限膨胀 |
| Caffeine + Redis 二级缓存 | ✅ 已实现 | CacheManager + HeavyKeeper 热 Key 提升 | HeavyKeeper.total 非线程安全 |
| Lua 脚本原子性 | ✅ 已实现 | Redis Lua 脚本 | value=0 语义丢失 |
| 10 秒分桶 + 批量落库 | ✅ 已实现 | SyncThumb2DBJob( 10秒定时) | KEYS 命令阻塞 Redis |
| 补偿兜底 | ✅ 已实现 | SyncThumb2DBCompensatoryJob(每日2点) | 异步删除无重试 |
| HeavyKeeper Top-K | ✅ 已实现 | 指数衰减 + 定时 fading | minHeap 查找 O(k)、指纹碰撞 |
| 分布式锁 | ✅ 已实现 | Redis SETNX + Lua 脚本释放 | 锁续期问题、Redis 单点故障 |
---
## 📁 关键文件索引
| 文件 | 职责 |
|------|------|
| CacheManager.java | 二级缓存管理器(Caffeine + Redis + 热 Key 提升 + 空值短缓存 + 互斥锁防击穿) |
| BloomFilterManager.java | 布隆过滤器管理器(防缓存穿透) |
| HeavyKeeper.java | HeavyKeeper Top-K 热 Key 探测算法 |
| RedisLockUtil.java | Redis 分布式锁工具(SETNX + Lua 释放) |
| ThumbServiceImpl.java | DB 优先方案(分布式锁 + 编程式事务 + 二级缓存 + 布隆过滤器) |
| ThumbServiceRedisImpl.java | Redis 优先方案(Lua 脚本 + 时间片分桶) |
| RedisLuaScriptConstant.java | Lua 脚本定义(原子点赞/取消点赞) |
| SyncThumb2DBJob.java | 10 秒定时批量落库任务 |
| SyncThumb2DBCompensatoryJob.java | 每日补偿兜底任务 |
---
## 🔗 相关链接
- [整体架构设计 ](architecture.md ) — 分层架构、数据库设计、两套方案对比
- [缓存系统设计 ](cache-system.md ) — 二级缓存架构、HeavyKeeper 算法详解
- [并发控制与数据一致性 ](data-consistency.md ) — 时间片分桶、定时任务、最终一致性保障
- [简历技术要点 ](resume.md ) — 5 个核心技术要点提炼