当前位置: 首页 > news >正文

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脚本 = 三重防护
    • 适用于高并发、高可用的生产环境

http://www.cnnetsun.cn/news/3490745.html

相关文章:

  • 闲谈《道德经》022|曲全枉直
  • 如何用Translumo实时屏幕翻译打破语言障碍:免费开源工具的完整使用指南
  • AI时代的程序员修养:高并发不是加机器那么简单
  • 三分钟搞定OFD转PDF:告别格式困扰的终极解决方案
  • uos-uwsgi-exporter安全特性详解:路径脱敏与敏感信息保护机制
  • Claude Code v2.1.211安装配置与AI编程助手实战指南
  • 如何完整备份微信聊天记录:WeChatMsg终极数据保存指南
  • 终极指南:如何用Navicat重置脚本实现Mac版无限试用
  • Win-Capture-Audio:精准捕获Windows应用音频的OBS插件解决方案
  • 【AI SaaS盈利暗箱】:AWS账单优化、LLM推理成本压缩、ARR增长飞轮——三位独角兽CTO联合披露
  • 孤能子视角:孙子兵法之虚实篇——耦合密度分布感知与方向锚切换
  • 【新手友好型 AI 智能体】 OpenClaw,Windows 端部署报错全解决(含安装包)
  • 3个秘诀:如何用gInk屏幕标注工具彻底改变你的工作演示效率
  • 远可拾8米,近可抗风噪,中间还能定向隔音——这颗37.5mm的小模组,凭什么是全双工通话的“最优解“?
  • Unity 2D RPG《英勇之地》源码解析:架构、系统设计与二次开发实战
  • 为什么你的Coze Bot记不住用户偏好?——基于Redis+LLM Embedding的记忆增强方案(附压测数据对比)
  • Linux GPIO驱动开发实战与优化技巧
  • 3分钟解决苹果设备Windows连接问题:一键安装驱动完整指南
  • 10分钟打造专属AI歌手:RVC WebUI语音克隆实战指南
  • openEuler Native-Turbo-kernel社区贡献指南:如何参与性能优化开发
  • 电商主图需要800×800但只有400×400?AI图片放大工具3步搞定
  • Stable Diffusion LoRA微调全链路拆解:从数据清洗到显存优化,手把手教你零基础训出商用级模型
  • Translumo终极指南:Windows平台最强实时屏幕翻译工具如何解决你的语言障碍
  • 将手机摄像头变成专业直播源:DroidCam OBS插件全攻略
  • 新版C4爆炸机制解析:全范围伤害原理与实战应对策略
  • 3分钟解锁Navicat无限试用:Mac用户必备的终极重置指南
  • 5个进阶技巧:掌握res-downloader资源嗅探与智能下载核心技术
  • 终极解决方案:让Windows资源管理器完美显示iPhone的HEIC照片缩略图
  • Legacy iOS Kit:让旧款iPhone/iPad重获新生的终极iOS降级工具
  • 053、LSC镜头阴影校正:从理论模型到产线标定