
1. 问题背景与现象分析最近在金融支付系统压测时发现一个诡异的业务编码重复问题在40-80并发量的压力下系统生成的交易流水号S202603170005出现重复。这个看似简单的唯一编码生成问题实际上暴露了我们在高并发场景下的设计缺陷。流水号生成逻辑原本是这样的public String generateSerialNo() { String prefix S DateUtil.format(new Date(), yyyyMMdd); int maxNo queryMaxNoFromDB(prefix); // 查询当日最大序号 return prefix String.format(%04d, maxNo 1); }在单线程环境下这段代码完美运行了两年。但当并发请求到来时多个线程同时执行queryMaxNoFromDB()获取到的maxNo相同导致最终生成的流水号重复。这就是典型的先查后改并发问题。2. 并发问题本质解析2.1 并发编程三大问题这个案例集中体现了并发编程的三大核心问题竞态条件多个线程对共享资源数据库记录的非原子操作内存可见性一个线程对maxNo的修改不能及时被其他线程感知指令重排序编译器优化可能导致代码执行顺序与预期不符2.2 问题重现实验通过以下测试代码可以稳定复现该问题Test public void testConcurrentGenerate() throws InterruptedException { final int threadCount 50; final CountDownLatch latch new CountDownLatch(threadCount); final SetString generatedCodes Collections.synchronizedSet(new HashSet()); for (int i 0; i threadCount; i) { new Thread(() - { String code generateSerialNo(); if (!generatedCodes.add(code)) { System.err.println(重复编码: code); } latch.countDown(); }).start(); } latch.await(); System.out.println(生成唯一编码数量: generatedCodes.size()); }3. Synchronized解决方案实战3.1 基础同步方案最直接的解决方案是使用synchronized关键字public synchronized String generateSerialNo() { // 原有逻辑 }这种方案的特点优点实现简单线程安全有保障缺点性能较差TPS从1200降到300左右适用场景低并发量100TPS的业务系统3.2 双重检查锁定优化针对性能问题可以采用更精细化的锁控制public String generateSerialNo() { String prefix S DateUtil.format(new Date(), yyyyMMdd); synchronized (this) { int maxNo queryMaxNoFromDB(prefix); return prefix String.format(%04d, maxNo 1); } }3.3 锁粒度优化实践进一步优化锁粒度使用前缀作为锁对象private static final ConcurrentHashMapString, Object lockMap new ConcurrentHashMap(); public String generateSerialNo() { String prefix S DateUtil.format(new Date(), yyyyMMdd); Object lock lockMap.computeIfAbsent(prefix, k - new Object()); synchronized (lock) { int maxNo queryMaxNoFromDB(prefix); return prefix String.format(%04d, maxNo 1); } }这种方案的特点优点不同日期的流水号生成互不影响缺点需要维护锁对象集合适用场景高并发且日期分散的业务4. 锁机制深度解析4.1 Synchronized实现原理通过javap反编译可以看到synchronized关键字的底层实现monitorenter // 获取对象监视器锁 ...代码块... monitorexit // 释放锁锁升级过程 无锁 → 偏向锁 → 轻量级锁 → 重量级锁4.2 锁性能对比测试不同方案的压测结果对比JMeter 100并发方案TPS平均响应时间错误率无同步150068ms42%方法同步320310ms0%代码块同步850118ms0%分段锁120083ms0%5. 生产环境解决方案5.1 分布式ID生成方案对于分布式系统推荐使用以下方案雪花算法64位ID 时间戳 机器ID 序列号数据库序列使用数据库的sequence特性Redis原子操作INCR命令生成序列号5.2 最终采用的混合方案我们实际采用的解决方案public String generateSerialNo() { // 90%请求走本地原子变量 if(ThreadLocalRandom.current().nextDouble() 0.9) { return localAtomicGenerator.get(); } // 10%请求同步更新数据库 synchronized (this) { String dbNo databaseGenerator.get(); localAtomicGenerator.set(dbNo); return dbNo; } }6. 经验总结与避坑指南锁范围要最小化只同步必要的代码块避免锁嵌套容易导致死锁注意锁对象选择String.intern()有性能风险监控锁竞争JStack查看线程阻塞情况典型错误案例// 错误1锁对象不可控 synchronized(dateFormat) { ... } // 错误2锁粒度太大 public synchronized void businessMethod() { ... } // 错误3锁逃逸 private final Object lock new Object(); public void method() { synchronized(new Object()) { ... } // 无意义 }7. 扩展思考与优化方向无锁方案探索CAS原子变量实现private final AtomicInteger counter new AtomicInteger(); public String generateNo() { return prefix counter.incrementAndGet(); }批量预生成提前生成一批序号缓存业务维度隔离不同业务线使用不同编号段在实际项目中我们通过引入Redis集群本地缓存的二级生成方案最终将系统吞吐量提升到5000TPS同时保证了编号的全局唯一性。这个优化过程让我深刻体会到并发问题的解决没有银弹必须根据具体业务场景选择最适合的方案。