Redisson分布式锁与布隆过滤器实战:12306项目中的高并发防护方案
前言
在分布式系统中,高并发场景下的数据一致性和缓存安全性是两个永恒的话题。本文将以一个真实的12306火车票项目为背景,深入剖析两个核心技术:Redisson分布式锁和布隆过滤器在实际项目中的应用。
项目技术栈:
- Java 17 + Spring Boot 3.0.7
- Redisson 3.21.3
- ShardingSphere 5.3.2
- RocketMQ 2.2.3
一、Redisson分布式锁:为什么不用原生Redis?
1.1 分布式锁的三大痛点
在分布式系统中,简单的SETNX命令并不能解决所有问题:
| 可重入性 | ❌ 需手动实现 | ✅ 内置支持 |
| 锁过期时间 | ❌ 需手动设置,可能提前过期 | ✅ 看门狗自动续期 |
| 分布式集合 | ❌ 无 | ✅ RMap/RSet/RDeque |
1.2 项目实战:订单状态反转
场景描述:在订单系统中,需要防止多个请求同时修改订单状态,导致数据不一致。
源码位置:services/order-service/…/service/impl/OrderServiceImpl.java
`java
@Override
public void reversal(OrderStatusReversalDTO requestParam) {
// 1. 获取分布式锁
RLock lock = redissonClient.getLock(
StrBuilder.create(“order:status-reversal:order_sn_”)
.append(requestParam.getOrderSn()).toString()
);
// 2. 尝试获取锁(非阻塞)
if (!lock.tryLock()) {
log.warn("订单重复修改状态,状态反转请求参数:{}",
JSON.toJSONString(requestParam));
}
try {
// 3. 更新订单状态
OrderDO updateOrderDO = new OrderDO();
updateOrderDO.setStatus(requestParam.getOrderStatus());
LambdaUpdateWrapper<OrderDO> updateWrapper = Wrappers.lambdaUpdate(OrderDO.class)
.eq(OrderDO::getOrderSn, requestParam.getOrderSn());
int updateResult = orderMapper.update(updateOrderDO, updateWrapper);
if (updateResult <= 0) {
throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_STATUS_REVERSAL_ERROR);
}
// 4. 更新订单项状态
OrderItemDO orderItemDO = new OrderItemDO();
orderItemDO.setStatus(requestParam.getOrderItemStatus());
LambdaUpdateWrapper<OrderItemDO> orderItemUpdateWrapper = Wrappers.lambdaUpdate(OrderItemDO.class)
.eq(OrderItemDO::getOrderSn, requestParam.getOrderSn());
int orderItemUpdateResult = orderItemMapper.update(orderItemDO, orderItemUpdateWrapper);
if (orderItemUpdateResult <= 0) {
throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_STATUS_REVERSAL_ERROR);
}
} finally {
// 5. 释放锁
lock.unlock();
}
}
`
关键点:
- 使用ryLock()非阻塞获取锁,避免线程等待
- inally块中必须释放锁,防止死锁
- 锁的粒度是订单号级别,不影响其他订单操作
1.3 项目实战:全局ID生成器节点分配
场景描述:在雪花算法中,需要确保每个节点获得唯一的节点ID(0-31)。
源码位置:services/order-service/…/service/orderid/OrderIdGeneratorManager.java
`java
@Component
@RequiredArgsConstructor
public final class OrderIdGeneratorManager implements InitializingBean {
private final RedissonClient redissonClient;
private final DistributedCache distributedCache;
private static DistributedIdGenerator DISTRIBUTED_ID_GENERATOR;
@Override
public void afterPropertiesSet() throws Exception {
String LOCK_KEY = "distributed_id_generator_lock_key";
RLock lock = redissonClient.getLock(LOCK_KEY);
// 加锁
lock.lock();
try {
StringRedisTemplate instance = (StringRedisTemplate) distributedCache.getInstance();
String DISTRIBUTED_ID_GENERATOR_KEY = "distributed_id_generator_config";
// Redis自增获取节点ID
long incremented = Optional.ofNullable(
instance.opsForValue().increment(DISTRIBUTED_ID_GENERATOR_KEY)).orElse(0L);
int NODE_MAX = 32;
if (incremented > NODE_MAX) {
incremented = 0;
instance.opsForValue().set(DISTRIBUTED_ID_GENERATOR_KEY, "0");
}
DISTRIBUTED_ID_GENERATOR = new DistributedIdGenerator(incremented);
} finally {
// 解锁
lock.unlock();
}
}
}
`
关键点:
- 使用lock()阻塞式获取锁,确保只有一个节点能初始化ID生成器
- 看门狗机制自动续期,防止业务执行期间锁过期
- 节点ID循环使用,支持32个节点
二、布隆过滤器:缓存穿透的终极解决方案
2.1 什么是缓存穿透?
场景描述:恶意用户频繁查询不存在的数据,每次请求都打到数据库。
`
正常流程:
用户请求 → 查缓存 → 命中 → 返回数据
缓存穿透:
用户请求 → 查缓存(不存在)→ 查数据库(不存在)→ 返回null
↑ 每次请求都这样,数据库压力巨大
`
2.2 布隆过滤器原理
布隆过滤器是一种概率型数据结构,用于判断元素是否在集合中:
`
核心特性:
✅ 如果布隆过滤器说"不存在" → 100%确定不存在
❌ 如果布隆过滤器说"存在" → 可能有误判(假阳性)
适用场景:
- 缓存穿透防护
- 垃圾邮件过滤
- 爬虫URL去重
`
2.3 项目配置:用户注册布隆过滤器
源码位置:services/user-service/…/config/UserRegisterBloomFilterProperties.java
`java
@Data
@ConfigurationProperties(prefix = UserRegisterBloomFilterProperties.PREFIX)
public final class UserRegisterBloomFilterProperties {
public static final String PREFIX = "framework.cache.redis.bloom-filter.user-register";
/**
* 布隆过滤器实例名称
*/
private String name = "user_register_cache_penetration_bloom_filter";
/**
* 每个元素的预期插入量
*/
private Long expectedInsertions = 64L;
/**
* 预期错误概率(默认3%)
*/
private Double falseProbability = 0.03D;
}
`
配置文件:pplication.yml
yaml framework: cache: redis: bloom-filter: user-register: name: user_register_cache_penetration_bloom_filter expected-insertions: 1000000 # 预期100万用户 false-probability: 0.01 # 误判率1%
2.4 项目配置:布隆过滤器Bean
源码位置:services/user-service/…/config/RBloomFilterConfiguration.java
`java
@Configuration
@EnableConfigurationProperties(UserRegisterBloomFilterProperties.class)
public class RBloomFilterConfiguration {
@Bean
public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(
RedissonClient redissonClient,
UserRegisterBloomFilterProperties properties) {
// 1. 获取布隆过滤器实例
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(
properties.getName()
);
// 2. 初始化布隆过滤器
// expectedInsertions: 预计插入元素数量
// falseProbability: 误判率
bloomFilter.tryInit(
properties.getExpectedInsertions(),
properties.getFalseProbability()
);
return bloomFilter;
}
}
`
2.5 项目实战:用户注册防穿透
场景描述:在用户注册流程中,需要先检查用户是否存在,避免直接查询数据库。
源码位置:services/user-service/…/service/impl/UserServiceImpl.java
`java
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter;
@Override
public UserQueryRespDTO queryUserByUsername(String username) {
// 1. 布隆过滤器预检查
if (!userRegisterCachePenetrationBloomFilter.contains(username)) {
// 布隆过滤器说"不存在" → 100%确定不存在
throw new ClientException("用户不存在,请检查用户名是否正确");
}
// 2. 查缓存
String cacheKey = "user:info:" + username;
UserQueryRespDTO cached = cache.get(cacheKey, UserQueryRespDTO.class);
if (cached != null) {
return cached;
}
// 3. 查数据库
LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
.eq(UserDO::getUsername, username);
UserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO == null) {
// 4. 数据库也不存在,加入布隆过滤器
userRegisterCachePenetrationBloomFilter.add(username);
throw new ClientException("用户不存在,请检查用户名是否正确");
}
// 5. 存入缓存
cache.put(cacheKey, BeanUtil.convert(userDO, UserQueryRespDTO.class));
return BeanUtil.convert(userDO, UserQueryRespDTO.class);
}
}
`
关键点:
- 布隆过滤器作为第一道防线,快速过滤不存在的请求
- 即使布隆过滤器说"存在",仍需查缓存和数据库
- 数据库不存在的用户也要加入布隆过滤器,防止重复查询
2.6 多级缓存框架的完整防护
源码位置:rameworks/cache/…/StringRedisTemplateProxy.java
`java
@Override
public T safeGet(String key, Class clazz, CacheLoader cacheLoader,
long timeout, TimeUnit timeUnit,
RBloomFilter bloomFilter,
CacheGetFilter cacheCheckFilter,
CacheGetIfAbsent cacheGetIfAbsent) {
// 1. 先查缓存
T result = get(key, clazz);
// 2. 缓存命中或布隆过滤器判断不存在 → 直接返回
if (!CacheUtil.isNullOrBlank(result)
|| Optional.ofNullable(cacheGetFilter).map(each -> each.filter(key)).orElse(false)
|| Optional.ofNullable(bloomFilter).map(each -> !each.contains(key)).orElse(false)) {
return result;
}
// 3. 获取分布式锁,防止缓存击穿
RLock lock = redissonClient.getLock(
SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key
);
lock.lock();
try {
// 4. 双重判定,减轻数据库压力
if (CacheUtil.isNullOrBlank(result = get(key, clazz))) {
// 5. 加载数据并设置缓存
if (CacheUtil.isNullOrBlank(result = loadAndSet(
key, cacheLoader, timeout, timeUnit, true, bloomFilter))) {
// 6. 数据为空,执行后置操作
Optional.ofNullable(cacheGetIfAbsent).ifPresent(each -> each.execute(key));
}
}
} finally {
lock.unlock();
}
return result;
}
`
防护流程:
请求到达 ↓ 布隆过滤器预检查(防穿透) ↓ 查缓存 ↓ 缓存命中? → 是 → 返回数据 ↓否 获取分布式锁(防击穿) ↓ 双重判定(锁内再查缓存) ↓ 缓存仍未命中? → 是 → 加载数据 ↓ 设置缓存 + 加入布隆过滤器 ↓ 释放锁 ↓ 返回数据
三、实战案例:购票流程中的安全防护
3.1 场景描述
在用户购票流程中,需要同时使用分布式锁和布隆过滤器:
3.2 源码实现
源码位置:services/ticket-service/…/handler/ticket/tokenbucket/TicketAvailabilityTokenBucket.java
`java
@Component
@RequiredArgsConstructor
public final class TicketAvailabilityTokenBucket {
private final RedissonClient redissonClient;
private final DistributedCache distributedCache;
/**
* 获取车票余量令牌桶中的令牌访问
*/
public TokenResultDTO acquireTokenInBucket(PurchaseTicketReqDTO requestParam) {
StringRedisTemplate stringRedisTemplate =
(StringRedisTemplate) distributedCache.getInstance();
// 1. 构造锁Key
String lockKey = "lock:ticket:token:" + requestParam.getTrainId();
RLock lock = redissonClient.getLock(lockKey);
// 2. 尝试获取锁
if (!lock.tryLock()) {
log.warn("获取车票令牌锁失败,车次ID:{}", requestParam.getTrainId());
return TokenResultDTO.builder().success(Boolean.FALSE).build();
}
try {
// 3. 执行令牌扣减逻辑
String tokenBucketHashKey = TICKET_AVAILABILITY_TOKEN_BUCKET + requestParam.getTrainId();
List<RouteDTO> takeoutRouteDTOList = trainStationService
.listTakeoutTrainStationRoute(
requestParam.getTrainId(),
requestParam.getDeparture(),
requestParam.getArrival()
);
// 4. 执行Lua脚本(原子操作)
DefaultRedisScript<String> redisScript = Singleton.get(
LUA_TICKET_AVAILABILITY_TOKEN_BUCKET_PATH, () -> {
DefaultRedisScript<String> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource(LUA_TICKET_AVAILABILITY_TOKEN_BUCKET_PATH)
));
script.setResultType(String.class);
return script;
}
);
String resultStr = stringRedisTemplate.execute(redisScript,
Lists.newArrayList(tokenBucketHashKey, luaScriptKey),
JSON.toJSONString(seatTypeCountArray),
JSON.toJSONString(takeoutRouteDTOList)
);
return JSON.parseObject(resultStr, TokenResultDTO.class);
} finally {
// 5. 释放锁
lock.unlock();
}
}
}
`
四、总结
4.1 核心技术对比
| Redisson分布式锁 | 数据一致性 | 看门狗+可重入锁 | 低 |
| 布隆过滤器 | 缓存穿透 | 概率型数据结构 | 极低 |
| Lua脚本 | 原子操作 | Redis服务端执行 | 低 |
4.2 最佳实践
分布式锁:
- 尽量使用ryLock()非阻塞方式
- inally块中必须释放锁
- 锁的粒度要细,避免影响其他业务
布隆过滤器:
- 合理设置expectedInsertions和alseProbability
- 定期重建布隆过滤器,避免内存泄漏
- 布隆过滤器说"不存在"是100%准确的
组合使用:
- 布隆过滤器 + 分布式锁 + Lua脚本 = 三重防护
- 适用于高并发、高可用的生产环境


