@项目线程池优化及库存行锁优化

线程池

项目中使用Executors的方式创建定长为20的线程,意味着使用了线程池后同一个时刻只能处理20个请求,其余的请求都会放入阻塞队列中进行排队;在此系统的设计中如果请求量超出了阻塞队列承受范围,多出的这些请求如果系统都无法承载这些量,如果不拒绝处理系统就挂了,所以设计上秉承着宁可拒绝保证系统正常运行也不能让系统挂掉。

具体的分析:线程池中有一个等待队列,就是用blockqueue实现的,将任务提交给线程池,线程池中可执行线程沾满后会将任务放到等待队列中,这样做就等于是限制了用户并发的流量,使得其在线程池的等待队列中排队处理。

代码解析:

在这里使用Future是为了让前端用户在调用Controller后可以同步的获得执行的结果;因为Callable是可以异步的返回计算结果。具体的代码实现是将要下单处理的流程使用submit()方法向线程池中添加任务;submit()有返回值,返回值对象是Future。submit执行的任务,可以通过Future对象的get方法接收抛出的异常,再抛出到异常处理层(本项目对所有异常集中统一处理)。

查看Executors的源码可知,实际还是调用了ThreadPoolExecutor类去构造线程池。

后续对线程池的深层次理解:

如何创建线程池

在《阿里巴巴Java开发手册》中,明确禁止使用Executors创建线程池,并要求开发者直接使用ThreadPoolExector或ScheduledThreadPoolExecutor进行创建。这样做是为了强制开发者明确线程池的运行策略,使其对线程池的每个配置参数皆做到心中有数,以规避因使用不当而造成资源耗尽的风险。

面试官:为什么《阿里巴巴Java开发手册》上要禁止使用Executors来创建线程池 - 掘金

就本项目而言,设置了拥塞窗口的为20的等待队列,在秒杀场景下,很容易堆积大量的请求,系统设计上秉承着宁可拒绝保证系统正常运行也不能让系统挂掉,所以当并发量很大时,现在的秒杀设计其实很容易出现问题的。

本项目中关于线程池中使用不好的点:

FixedThreadPool线程池里的BlockQueue是无界队列,要是线程执行的接口被阻塞会导致BlockQueue无限增加导致内存OOM;具体看下面的文章。

线程池使用的10个坑 - 掘金

更好的做法:

自定义ThreadPoolExecutor中的corePoolSize,maximumPoolSize,keepAliveTime,还有阻塞队列大小这些参数一般没有固定公式,需要根据硬件、压测情况等的测试结论不断修正的数值

ThreadPoolExecutor pool = new ThreadPoolExecutor(10,30,1, 
TimeUnit.MINUTES,newArrayBlockingQueue<>(20));

线程池在业务中的实践

场景1:快速响应用户请求

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

分析:从用户体验角度看,结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

场景2:快速处理批量任务

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

实际问题及方案思考

线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。看下面美团技术团队的文章。

Java线程池实现原理及其在美团业务中的实践 - 掘金

面试必备:Java线程池解析 - 掘金 (juejin.cn)-------确实是面试必备

线程池没你想的那么简单 - 掘金 (juejin.cn)

线程池没你想的那么简单(续) - 掘金 (juejin.cn)---->>>

  • 执行带有返回值的线程。
  • 异常处理怎么办?
  • 所有任务执行完怎么通知我?

面试官:来!聊聊线程池的实现原理以及使用时的问题 - 掘金 (juejin.cn)----推荐的文章不错

《我们一起进大厂》系列-秒杀系统设计 - 掘金 (juejin.cn)

库存行锁优化

库存行锁优化的好处:

行锁相较于表锁,行锁的的粒度更细,并发度更高,但也意味着更大的锁开销。对于没有行锁的引擎,例如MyISAM来说,同一时间对一个表的更新只能有一个执行。显而易见,处于对秒杀业务高并发度的要求,行锁的出现是必要的。

回顾之前减库存的操作:

<update id="decreaseStock">
  update item_stock
  set stock = stock - #{amount}
  where item_id = #{itemId} and stock >= #{amount}
</update>

库存的数量就是stock-amount,条件是商品itemId和stock的大于amount,当给item_id加上唯一索引,这样查询的时候为数据库加上行锁,否则是数据库表锁

为啥给item_id加上唯一索引,查询的时候为数据库就是行锁?

InnoDB和MyISAM的最大不同点有两个:

一:InnoDB支持事务(transaction);

二:默认采用行级锁加锁,这样可以保证事务的一致性;

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。

行锁介绍:

行锁的劣势:开销大;加锁慢;会出现死锁

行锁的优势:锁的粒度小,发生锁冲突的概率低;处理并发的能力强

加锁的方式:InnoDB默认采用行级锁加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然程序员也可以显示的加锁;

唯一索引存在的快慢问题

唯一索引有个很大的好处,就是查询数据时会比普通索引效率更高,因为基于普通索引的字段查询数据时,当查询到一条数据后,会继续走完整个索引树,因为可能会存在多条字段值相同的数据。 但如果字段上建立的是唯一索引,当找到一条数据后就会立马停下检索,因此本身建立唯一索引的字段值就具备唯一性。

但插入数据时就不同了,因为要确保数据不重复,所以插入前会检查一遍表中是否存在相同的数据。但普通索引则不需要考虑这个问题,因此普通索引的数据插入会快一些。但秒杀场景下更多的是查询操作,所以唯一索引很适合。

唯一索引在创建时,需要通过UNIQUE关键字创建

CREATE UNIQUE INDEX indexName ON tableName (columnName(length));

主键索引其实是一种特殊的唯一索引,但主键索引却并不是通过UNIQUE关键字创建的,而是通过PRIMARY关键字创建:

ALTER TABLE tableName ADD PRIMARY KEY indexName(columnName);

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值