12306项目学习day2

📅 2026/7/18 14:59:29 👁️ 阅读次数 📝 编程学习
12306项目学习day2

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命令并不能解决所有问题:

问题原生RedisRedisson
可重入性❌ 需手动实现✅ 内置支持
锁过期时间❌ 需手动设置,可能提前过期✅ 看门狗自动续期
分布式集合❌ 无✅ 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 场景描述

在用户购票流程中,需要同时使用分布式锁和布隆过滤器:

  1. 分布式锁:防止超卖(多个用户同时购买同一张票)
  2. 布隆过滤器:快速判断车次是否存在

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 最佳实践

  1. 分布式锁

    • 尽量使用 ryLock()非阻塞方式
    • inally块中必须释放锁
    • 锁的粒度要细,避免影响其他业务
  2. 布隆过滤器

    • 合理设置expectedInsertions和alseProbability
    • 定期重建布隆过滤器,避免内存泄漏
    • 布隆过滤器说"不存在"是100%准确的
  3. 组合使用

    • 布隆过滤器 + 分布式锁 + Lua脚本 = 三重防护
    • 适用于高并发、高可用的生产环境