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命令并不能解决所有问题:
| 问题 | 原生Redis | Redisson |
|---|---|---|
| 可重入性 | ❌ 需手动实现 | ✅ 内置支持 |
| 锁过期时间 | ❌ 需手动设置,可能提前过期 | ✅ 看门狗自动续期 |
| 分布式集合 | ❌ 无 | ✅ 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脚本 = 三重防护
- 适用于高并发、高可用的生产环境