当前位置: 首页 > 科技观察

秒杀系统:并发队列接口设计并发请求数据安全处理

时间:2023-03-21 11:20:07 科技观察

并发队列选择Java的concurrent包提供了三种常用的并发队列实现,分别是:ArrayBlockingQueue、ConcurrentLinkedQueue和LinkedBlockingQueue。ArrayBlockingQueue是一个初始容量固定的阻塞队列。我们可以将其作为数据库模块竞价成功的队列。比如有10个商品,那么我们设置一个大小为10的数组队列。ConcurrentLinkedQueue是使用CAS原语无锁队列实现的。它是一个异步队列。入队速度很快,出队时队列是锁死的,所以性能稍慢。LinkedBlockingQueue也是一个阻塞队列。锁定用于进入和退出队列。当队列为空时,线程会暂时阻塞。在请求预处理阶段,由于我们系统的入队需求远大于出队需求,所以队列一般不会为空,所以我们可以选择ConcurrentLinkedQueue作为我们的请求队列实现1.请求接口的合理设计一个秒杀或者抢购页面,通常分为两部分,一个是静态HTML等内容,一个是参与秒杀的web后台请求接口。通常静态HTML等内容都是通过CDN部署的,一般压力不大,核心瓶颈其实在后台请求接口上。这个后端接口必须能够支持高并发请求。同时,必须尽可能“快”,在最短的时间内返回用户的请求结果,这一点非常重要。为了尽可能快地实现这一点,最好对接口的后端存储使用内存级操作。直接针对MySQL等存储还是不合适的。如果有这么复杂的业务需求,建议使用异步写法。当然,也有一些秒杀、抢购使用了“滞后反馈”,也就是说秒杀不知道此刻的结果,需要一段时间才能看到用户秒杀成功与否。不是来自页面。但这种行为属于“偷懒”行为,同时用户体验不佳,容易被用户视为“暗箱操作”。高并发下的数据安全我们知道,当多个线程写入同一个文件时,会存在“线程安全”问题(多个线程同时运行同一段代码,如果每次运行的结果与结果不同单线程运行的一样,结果和预期的一样,是线程安全的)。如果是MySQL数据库,可以利用其内置的锁机制很好的解决问题。但是在大规模并发场景下,不推荐使用MySQL。在闪购、抢购场景中,还有一个问题,就是“超发”。如果这方面控制不慎,就会出现超发。我们也听说过一些电商搞抢购活动,买家拍照成功后,商家不承认订单有效,拒绝发货。这里的问题不一定是商家背信弃义,而是系统技术层面的超发风险。1、超发的原因假设在抢购场景下,我们一共只有100件商品。最后一刻,我们消耗了99件物品,只剩下最后一件了。这时系统发送了多个并发请求,这些请求读取到的商品余额都是99,都通过了这个余额判断,最终导致超发。(同文前面提到的场景)上图中,并发用户B也“抢购成功”,让多了一个人拿到了商品。这种场景在高并发的情况下非常容易出现。2.悲观锁的思路解决线程安全的思路有很多,我们可以从“悲观锁”的方向开始讨论。悲观锁,即修改数据时,采用锁定状态,排除外部请求修改。当遇到锁定状态时,必须等待。虽然上面的方案确实解决了线程安全的问题,但是别忘了我们的场景是“高并发”的。也就是说,这样的修改请求会很多,每个请求都需要等待一个“锁”。有些线程可能永远没有机会抢到这个“锁”,这样的请求就会死在那里。同时,这样的请求会很多,会瞬间增加系统的平均响应时间。结果,可用连接数将被耗尽,系统将陷入异常。3.FIFO队列思路不错,所以我们稍微修改一下上面的场景,我们直接把请求放到队列中,使用FIFO(FirstInputFirstOutput,先进先出),这样,我们就不会导致一些请求一直没有获取到锁。看到这里,是不是觉得把多线程变成单线程有点勉强呢?那么,我们现在已经解决了锁的问题,所有的请求都在一个“先进先出”的队列中处理。那么一个新的问题出现了。在高并发场景下,由于请求量大,极有可能瞬间“爆”队列内存,进而导致系统陷入异常状态。或者设计一个巨大的内存队列也是一种解决方案。但是,系统在队列中处理请求的速度无法与疯狂涌入队列的数量相提并论。也就是说队列中的请求会越积越多,最终web系统的平均响应时间还是会大幅度下降,系统还是会陷入异常。4.乐观锁思想至此,我们就可以讨论一下“乐观锁”的思想了。乐观锁采用了比“悲观锁”更宽松的锁机制,大多更新一个版本号(Version)。实现是所有对该数据的请求都可以修改,但将获得数据的版本号。只有版本号匹配的才能更新成功,其他返回抢购失败。这种情况下我们可以不用考虑队列的问题,但是会增加CPU的计算开销。但是,总的来说,这是一个更好的解决方案。支持“乐观锁”功能的软件和服务有很多,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。