1.ConcurrentHashMap 里存了什么
java
private final ConcurrentHashMap<String, AccountQuota> cache = new ConcurrentHashMap<>();
程序启动后自动执行缓存类的预热方法,自动查询相关数据,预热数据到concurrentHashMap上
2.切片是切什么?
不是切预热后的concurrenthashmap,而是将其转化成arraylist(相当于拍了个照)后进行固定长度的切分
自己想:1.为什么快照?2.快照有什么代价?
3.切片为了干什么
切片之前:每次开票全表扫描
切片之后:可以多线程并行扫描切好的list即可
所以不仅利用concurrenthashmap做了数据预热(存在JVM堆内存中,比原来需要在mysql查询快),还通过切片实现了开票前的查询工作以及开票业务(真正耗时)的并发实现,缩短了时间
java
private Integer processChunk(List<String> chunk) {
int count = 0;
for (String accountId : chunk) {
// 从 ConcurrentHashMap 读取快照,并发安全
quotaCache.get(accountId);
count++;
}
simulateIo((long) count * props.getInvoice().getIoMsPerAccount());
return count;
}
4.做了缓存后开票和传统开票有啥区别
要先知道什么是开票:
一般不会改数据库中的数据,一般调用外部开票接口、更新【状态字段】、拿到发票文件链接,有的场景会生成发票 OFD/PDF 文档(大多是服务商返回文件链接,不是后端自己生成),这些任务都比较耗时
做了上述操作的缓存后就可以把得到的list进行切片了,切片后就可以多切片在不同线程并发开票
5.会有高并发的问题吗
高并发下必然发生死锁问题
定义 :多个进程 / 线程互相持有对方需要的资源,又都不肯释放自己占有的资源,导致所有进程都被阻塞、永远无法继续执行,这种僵局就是死锁。
这里的高并发指?
数据量上涨到10万?❌这叫大数据量不叫高并发
请求并发(高QPS)比如这里多个http请求❌不是这里的情况
共享数据并发 (比如这里多线程同时读
ConcurrentHashMap/AccountQuota)❌不是这里的情况池内并发(比如这里heavyPool里同时被占用的线程数)✅
**池内并发细讲:**父任务(一次完整的开票逻辑)和子任务(一个分片的开票逻辑)共用一个线程池
池内并发不是看 QPS 有多高,而是看同一时刻有多少个开票任务在 heavyPool 里同时执行。

小于总线程数,只会导致排队慢,大了则有概率触发死锁

多建几个线程池不就好了?
答案要分情况------大多数"多建几个 heavyPool"的拆法不但不降低死锁概率,反而会升高。而且即使降低了也是治标不治本,只是"炸弹延后",最好的解决办法不是加池而是改结构
1.不增反降(总线程数不变,仅做拆分池的数量)

2.纯加池
效果:故障隔离:池 A 死锁了,池 B 里的业务还能继续跑,服务整体不会 100% 挂。
(只有故障隔离吗:确实,肯定先紧着一个 池加父任务啊)
代价:
1.死锁的请求永远不返回:池B没有父任务(或者缺少了A中的失败父任务)实际未开票但是没报错及时返回
2.故障更加难以排查(轮询分配下,用户看到的是"随机三分之一的开票请求没响应,第二次重试又能成"------这种不可复现的间歇故障,排查成本远高于一次干脆的全挂。)
3.真正能解决死锁的加池
父子任务分池,但父任务的池相当于白白占用线程用来等待,太浪费资源,且父任务超过线程池最大线程数排队会更慢一个档次
限制入口的父任务并发数
让「父任务占用的线程数 < 池核心数」,相关设计后续给出

