资讯动态

优化开票任务Demo开发(章节2)

发布时间:2026/10/15 4:08:11 来源:尧图企业网站定制
听起来很复杂像是ConcurrentHashMap把线程隔离开了但其实就是创建了两个线程池这两个线程池本来就是隔开的跟ConcurrentHashMap没啥关系ConcurrentHashMap只是为了解决数据争用项目中通过一个Cache类把用户的相关开票数据搬到ConcurrentHashMap相当于缓存一下开票时不再查数据库直接读内存速度直接提升上百倍... public class QuotaCache { private final ConcurrentHashMapString, AccountQuota cache new ConcurrentHashMap(); ..... PostConstruct public void init() {...} /** 从数据库全量加载到内存缓存 */ public void reload() {...} public int size() { return cache.size(); } /** 返回快照 key 集合改进版据此分片 */ public ListString snapshotKeys() { return new ArrayList(cache.keySet()); } /** 并发安全地读取某个额度 */ public AccountQuota get(String accountId) {...} }切片把数据切成指定份数份数/大小可动态调切完每份提交给线程池让池子自己分配。但这里不是切原来的concurrenthhashmap而是复制版为什么要快照复制充值线程随时refresh()往里put新账号。直接切原件就乱了还会报异常new ArrayList(...)就是按下快门后面切分 提交 100 个任务全程不用加锁、不用担心数据变了。也有代价快照是弱一致的拍照时刚put进来、还没拷到的账号这次开票就漏了下次再开。需要忍受这点延迟任务如何路由到对应的线程池怎么执行后续逻辑chunkFutures.add(monitor.submit(heavyPool, improved-chunk, false, () - processChunk(chunk)));在提交数据切片的时候那行代码已经用lambda表达式指定干啥了所以这里ConcurrentHashMap的真是作用是做QuotaCache内存账本让多线程并行开票时读写安全又快。为什么将一个业务一个线程池改成了只有两个线程池一个业务一个池会线程爆炸每个ThreadPoolTaskExecutor都有自己的core/max/queue 线程。业务涨到 N 个空闲线程就是N*core上下文切换、内存、DB连接全被吃掉还每个池都要单独调参、单独监控TaskMonitor。真正要隔离的是快慢不是业务名痛点在service/LegacyInvoiceService.java——开票重任务1000账户*4ms4000ms和fast-order/recharge30ms挤在同一个fastPool重任务占满线程快任务排队触发alertThresholdMs1200ms的ALERT。所以只要把耗时长和耗时短分开就够了不需要充值一个池、查询一个池、下单一个池。两池现在的分工fastPoolLegacyInvoiceService/FastOrderService的所有短任务共享保证普通业务RT。heavyPool只给service/ImprovedInvoiceService.java用开票主任务拆出来的improved-chunk子任务全跑这里面内部可以固定大并发真正并行注释里还解释了为什么coremax避免coremax大队列时扩容太慢再大也不抢fastPool。简单说按业务分是横向膨胀按快慢分是纵向隔离两个池就解决了队头阻塞又保留了扩缩容和监控的简单性。为什么用ConcurrentHashMap为什么不用HashMaprunOneImprovedInvoice()流程是quotaCache.snapshotKeys()取全量key按chunkSize切分比如1000个账户/每片100 10个分片每个分片monitor.submit(heavyPool, improved-chunk, ... processChunk(chunk))扔到heavyPool并行跑这时多个heavyPool线程会同时processChunk()里quotaCache.get(accountId)并发读refresh()里cache.put()充值后并发写snapshotKeys()里new ArrayList(cache.keySet())弱一致性遍历HashMap在并发读写下会死循环/数据丢失/ConcurrentModificationExceptionHashtable/Collections.synchronizedMap是全表锁读也串行化10个分片并行就没意义了。ConcurrentHashMap是分段/CAS锁只有写同一bucket才竞争读几乎无锁还支持安全遍历正好匹配“多读少写分片并行”的开票场景。一句话用它来扛住heavyPool多线程同时读快照是改进版提速的基石。为什么不用HashTable不用Hashtable核心就一句话Hashtable是全表一把大锁并发一上来就把你heavyPool并行的优势全吃掉了而ConcurrentHashMap是细粒度锁读几乎无锁。在你这个项目里差距会被放大锁粒度大锁 vs 分段/CASHashtable所有get/put方法都加了synchronized整个表一把锁。10个improved-chunk分片线程同时quotaCache.get()也要排队一个一个进。ConcurrentHashMapJava8get无锁 volatile 读put只锁单个 bucket 用 CAS synchronized 头节点。不同accountId大概率落在不同 bucket10个线程真正并行。你QuotaCache.java是典型的多读少写init/reload/refresh偶尔写processChunk里高频getsnapshotKeys()遍历。Hashtable让读也串行完全违背“内存快照提速”的初衷。性能实测差距16线程下纯读ConcurrentHashMap通常是Hashtable的数倍到十几倍因为后者全在抢同一把监视器锁。总结对照维度HashtableConcurrentHashMap本项目用读并发全表锁串行无锁读并行写并发全表锁串行只锁 bucket并行所以要正确性Hashtable也能保证线程安全要你这个“动态拆分线程池隔离并行开票”的性能就必须用ConcurrentHashMap。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑