秒杀的场景有不少,好比:抢购、抢票、抢红包等等。总之,就是在极短期内有大量的请求。html
咱们都知道,这种系统设计的大方向就是限流,即经过层层过滤,最终只让相对较少的请求进入到核心业务处理层。mysql
这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确,就是已经到减库存那一步了,在这一步中若是保证不超卖。sql
用队列的话,能够是Java自动的队列,也能够用Redis的LPUSH RPOP数据库
重点是扣减库存多线程
我理解,主要的方式是加锁。加锁有两个层面:一个是程序层面,另外一个是数据库层面。并发
这种场景下应该不多有人用Java自带的锁(好比:synchronized、Lock)吧,由于它们只在同一个JVM内有效,若是你的应用部署了多台的话,应该用分布式锁。分布式
关于Redis分布式锁,能够看我以前的一篇《基于Redis的分布式锁的简单实现》测试
其实,这里加分布式锁就是将多线程请求转成单线程请求,由于每次只有一个线程得到锁并执行,其他都被阻塞了。优化
这里有一点须要注意,就是当你应用了事务的话可能会存在问题,请看下面的代码spa
可能有人会这样写,第一眼看起来挺好的,没问题啊,但仔细实践证实是由问题的。
咱们知道,mysql默认的事务隔离级别是REPEATABLE-READ
关于事务隔离级别这块儿,可参考《mysql事务隔离级别》
在这种隔离级别下,同一个事务中屡次读取,返回的数据是同样的
同时,Spring声明式事务默认的传播特性REQUIRED
Spring声明式事务是Spring AOP最好的例子,Spring是经过AOP代理的方式来实现事务的,也就是说在调用reduceStock()方法的以前就已经开启了事务。
那么,在并发状况下可能会存在这样的状况,假设线程T1和T2都执行到这里,因而它们都开启了事务S1和S2,T1先执行,T2后执行,
因为T2执行的时候事务已经建立了,根据隔离级别,这个时候事务S2读取不到S1已提交的数据,因而就会出现T1和T2读取到的值是同样的,即T2读取的是T1更新前的库存数据。
关于这一点,你们能够本身写个代码测试一下,下面是一段参考:
鉴于这种状况呢,能够将库存放到Redis中,咱们直接读写Redis,这样能够避免受数据库事务的影响,固然这也会带来新的问题,再也不讨论。
在Java中,一个线程想修改某个变量的值,那么第一步是将变量的值从主内存中读取到本身工做内存中,而后修改,最后写回主内存。这个过程能够归结为:读取——修改——写入,在写回内存的时候可能当前内存中那个值已经发生了变化,这个时候若是继续写则会覆盖别人的数据,只有当内存中的那个值和它修改以前读到的那个值同样,才能够写入。这个跟数据库是同样的。Java中经过Unsafe中compareAndSwapObject这样的方法类实现的,它直接调用CPU指令。
数据库中也有CAS,乐观锁就是一种CAS
数据增长一个版本标识,通常是经过为数据库表增长一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,若是数据库表当前版本号与第一次取出来的version值相等,则予以更新,不然认为是过时数据。
更新的时候带上版本号,只有当前版本号与更新以前查询时的版本一致,才会更新
这里顺便多提一句,CAS中的ABA问题
假设,原先的值是A,线程-1读取到的值是A,想把它改为D,可是在此期间,有可能其余线程已经屡次修改过这个值,只不过最后当线程-1准备将A改为D的时候,它发现刚好仍是A,觉得没有人改过,其实这时候的A已经不是原来的A了。
也就是说,尽管修改以前作了比较,固然,仍然会出现以下状况:
ABA问题致使的缘由,是CAS过程当中只简单进行了“值”的校验,有些状况下,“值”虽然相同,却已经不是原来的数据了。
CAS不能只比对“值”,还必须确保的是原来的数据,才能修改为功。
“版本号”的比对,一个数据一个版本,版本变化,即便值相同,也不该该修改为功。
不只要关注值,还要关注是否是原来的对象
基于“值”的CAS乐观锁,可能致使ABA问题。CAS乐观锁,必须保证修改时的“此数据”就是“彼数据”,应该由“值”比对,优化为“版本号”比对。
https://www.sohu.com/a/150900817_178889
https://blog.csdn.net/zhjunjun93/article/details/78560700
https://blog.csdn.net/rexct392358928/article/details/52230737