[SpringBoot][15][SpringBoot处理高并发]

第 15 章 SpringBoot处理高并发

在企业实际应用中,会遇到很多高并发场景,最典型的例子就是双十一的抢购。这时候,如果仅仅按照之前简单的方式进行处理,不仅性能无法保证,而且有可能导致数据库某些数据的超发。

下图演示了超发的一种情况

为此,我们提供了三种高并发场景下的方案:悲观锁、乐观锁和使用Redis

15.1 悲观锁

本节讨论使用悲观锁处理高并发超发的问题。在高并发中出现超发现象,根本在于共享的数据被多个线程所修改,无法保证其执行的顺序。为此,悲观锁给出的解决方案是:如果一个数据库事务读取到产品后,就将数据直接锁定,不允许别的线程进行读写操作,直至当前数据库事务完成才释放这条数据的锁,则不会出现之前看到的超发问题。

下面举个简单的实现方式

1
2
3
4
5
<select id="getProduct" parameterType="long" resultType="product">
select id, product_name as productName,
stock, price, from t_product
where id = #{id} for update
</select>

上面的SQL在最后加了for update就是使用数据库内部的锁对记录加锁,从而使得其他事务等待以保证数据一致性。

悲观锁的最大缺点是损失大量的性能

15.2 乐观锁

乐观锁是一种不使用数据库锁和不阻塞线程并发的方案。下面我们分步骤来解释乐观锁

第一步,我们想到一种最简单的方式,CAS(Compare And Swap):以本章的商品购买为例来说,就是一个线程一开始先读取既有的商品库存数据,保存起来们把这些旧数据称为旧值,然后去执行一定的业务逻辑,等到需要对共享数据做修改时,会事先保存的旧值库存与当前数据库的库存进行比较,如果旧值与当前库存一致,它就认为数据没有被改过,否则就认为数据已经被修改过,当前计算将不被信任,所以就不再修改任何数据。其流程图如下。

但是,这个方案会带来ABA问题

从表中可以看出,在T2到T5时刻,线程1计算商品总价格的时候,当前库存会被线程2所修改,它是一个A→B→A的过程,所以人们比较形象地称之为“ABA问题”。换句话说,线程1在计算商品总价格时,当前库存是一个变化的值,这样就可能出现错误的计算。

第二步,为了克服这个问题,我们引入了一些规则,典型的如增加版本号(version)。在数据库的商品表中多加一个字段叫做版本号,并且规定:只要操作过程中修改共享值,无论业务正常、回退还是异常,版本号只增不减。这时,按照第一步的思路,一开始我们读取商品库存,获得了版本号;当我们要修改这个库存时,再次读取库存,如果发现版本号变了,那么证明有其他线程修改过这个数据,则取消业务。

但是,这个方案的问题是,我们会频繁的取消业务,导致一定比例的用户请求被拒绝。

第三步,为了克服这个问题,乐观锁还可以引入重入机制,就是一旦更新失败,就重做一次。并且会使用限制时间或重入次数的方式压制过多的SQL执行

现在总结一下乐观锁的机制:乐观锁是一种不使用数据库锁的机制,并且不会造成线程的阻塞,只是采用多版本号机制来实现。但是,因为版本的冲突造成了请求失败的概率剧增,所以这时往往需要通过重入的机制将请求失败的概率降低。但是,多次的重入会带来过多执行SQL的问题。为了克服这个问题,可以考虑使用按时间戳或者限制重入次数的办法。可见乐观锁还是一个相对比较复杂的机制。

15.3 使用Redis处理高并发

我们也可以通过Redis Lua的原子性来处理高并发,并且由于Redis是基于内存的,通常大幅度提升性能。

下面描述简单的实现思路

  • 首先使用Redis代替数据库来响应高并发,将商品库存放入Redis中存储、访问、更新
  • 此外,因为Redis基于内存,为了防止数据丢失,设置一个定时任务,定时将数据从Redis存入数据库。