秒杀超卖 解决方案(史上最全)
作者:互联网
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源:
-
疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 《 Java 高并发 三部曲 》 面试必备 + 大厂必备 + 涨薪必备
-
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 +涨薪必备 免费领
-
疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 面试必备 + 大厂必备 + 涨薪必备 免费领
-
疯狂创客圈 资源宝库: Java 必备 百度网盘资源大合集 价值>1000元 【免费取 】
推荐: 疯狂创客圈 必看/必收/高质量/博文
史上最全 分布式锁 2 大篇 | 图解 + 史上最全 + 吐血推荐 |
---|---|
1:Redis 分布式锁 (图解-秒懂-史上最全) | 2:Zookeeper 分布式锁 (图解-秒懂-史上最全) |
史上最全 Java 面试题 21 个专题 | 阿里、京东、美团、头条.... 随意挑、横着走!!! |
---|---|
1: JVM面试题(史上最强、持续更新、吐血推荐) | 2:Java基础面试题(史上最全、持续更新、吐血推荐 |
3:架构设计面试题 (史上最全、持续更新、吐血推荐) | 4:设计模式面试题 (史上最全、持续更新、吐血推荐) |
17、分布式事务面试题 (史上最全、持续更新、吐血推荐) | 一致性协议 (史上最全) |
29、多线程面试题(史上最全) | 30、HR面经,过五关斩六将后,小心阴沟翻船! |
9.网络协议面试题(史上最全、持续更新、吐血推荐) | 更多专题, 请参见【 疯狂创客圈 高并发 总目录 】 |
分布式 高并发 必读 的精彩博文 | |
---|---|
nacos 实战(史上最全) | sentinel (史上最全+入门教程) |
Zookeeper 分布式锁 (图解+秒懂+史上最全) | Webflux(史上最全) |
SpringCloud gateway (史上最全) | TCP/IP(图解+秒懂+史上最全) |
10分钟看懂, Java NIO 底层原理 | Feign原理 (图解) |
大厂必备之:Reactor模式 | 更多精彩博文 ..... 请参见【 疯狂创客圈 高并发 总目录 】 |
前言
先来就库存超卖的问题作描述:一般电子商务网站都会遇到如团购、秒杀、特价之类的活动,而这样的活动有一个共同的特点就是访问量激增、上千甚至上万人抢购一个商品。然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。
在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。
一、问题描述
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。问题:
当商品A一共有库存15件,用户甲先下单10件,用户乙下单8件,这时候库存只能满足一个人下单成功,如果两个人同时提交,就出现了超卖的问题。
二、解决的三种方案
- 悲观锁
通过悲观锁解决超卖
- 乐观锁
通过乐观锁解决超卖
- 分段执行的排队方案
通过分段执行的排队方案解决超卖
解决方案1: 悲观锁
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法:
beginTranse(开启事务)
try{
query('select amount from s_store where goodID = 12345');
if(库存 > 0){
//quantity为请求减掉的库存数量
query('update s_store set amount = amount - quantity where goodID = 12345');
}
}catch( Exception e ){
rollBack(回滚)
}
commit(提交事务)
问题:
注意,上面的代码容易出现死锁,采用不多。
解决方案:一般提前采用 select for update,提前加上写锁。
beginTranse(开启事务)
try{
query('select amount from s_store where goodID = 12345 for update');
if(库存 > 0){
//quantity为请求减掉的库存数量
query('update s_store set amount = amount - quantity where goodID = 12345');
}
}catch( Exception e ){
rollBack(回滚)
}
commit(提交事务)
行锁和表锁
行锁:分为 共享锁 和 排它锁。
共享锁又称:读锁。当一个事务对某几行上读锁时,允许其他事务对这几行进行读操作,但不允许其进行写操作,也不允许其他事务给这几行上排它锁,但允许上读锁。
上共享锁的写法:lock in share mode
例如: select math from zje where math>60 lock in share mode;
排它锁又称:写锁。当一个事务对某几个上写锁时,不允许其他事务写,但允许读。更不允许其他事务给这几行上任何锁。包括写锁。
上排它锁的写法:for update
例如:select math from zje where math >60 for update;
死锁
死锁:例如说两个事务,事务A锁住了15行,同时事务B锁住了610行,此时事务A请求锁住610行,就会阻塞直到事务B施放610行的锁,而随后事务B又请求锁住15行,事务B也阻塞直到事务A释放15行的锁。死锁发生时,会产生Deadlock错误。
表锁:不会出现死锁,发生锁冲突几率高,并发低。
表锁是对表操作的,所以自然锁住全表的表锁就不会出现死锁。但是表锁效率低。
行锁:会出现死锁,发生锁冲突几率低,并发高。
3.行锁的要点
注意几点:
1.行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
2.两个事务不能锁同一个索引,例如:
事务A先执行:
select math from zje where math>60 for update;
事务B再执行:
select math from zje where math<60 for update;
这样的话,事务B是会阻塞的。如果事务B把 math索引换成其他索引就不会阻塞,
但注意,换成其他索引锁住的行不能和math索引锁住的行有重复。
3.insert ,delete , update在事务中都会自动默认加上排它锁。
实现:
会话1: | 会话2: |
---|---|
begin; select math from zje where math>60 for update; |
begin; update zje set math=99 where math=68; 阻塞 |
MyISAM与InnoDB 的区别
MyISAM:MyISAM是默认存储引擎(Mysql5.1前),每个MyISAM在磁盘上存储成三个文件,每一个文件的名字均以表的名字开始,扩展名指出文件类型。
.frm文件存储表定义
·MYD (MYData)文件存储表的数据
.MYI (MYIndex)文件存储表的索引
InnoDB:MySQL的默认存储引擎,给 MySQL 提供了具有事务(transaction)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)、多版本并发控制(multi-versioned concurrency control)的事务安全(transaction-safe (ACID compliant))型表。InnoDB 提供了行级锁(locking on row level),提供与 Oracle 类似的不加锁读取(non-locking read in SELECTs)。
MyISAM与InnoDB 的区别
-
InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
-
InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
-
聚集索引 VS 非聚集索引
InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。
InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值
but, MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
总结
也就是说:InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。
4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快(注意不能加有任何WHERE条件);
那么为什么InnoDB没有了这个变量呢?
因为InnoDB的事务特性,在同一时刻表中的行数对于不同的事务而言是不一样的,因此count统计会计算对于当前事务而言可以统计到的行数,而不是将总行数储存起来方便快速查询。InnoDB会尝试遍历一个尽可能小的索引除非优化器提示使用别的索引。如果二级索引不存在,InnoDB还会尝试去遍历其他聚簇索引。
如果索引并没有完全处于InnoDB维护的缓冲区(Buffer Pool)中,count操作会比较费时。可以建立一个记录总行数的表并让你的程序在INSERT/DELETE时更新对应的数据。和上面提到的问题一样,如果此时存在多个事务的话这种方案也不太好用。如果得到大致的行数值已经足够满足需求可以尝试SHOW TABLE STATUS
5. Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引领域的查询效率上MyISAM速度更快高;PS:5.7以后的InnoDB支持全文索引了
6. MyISAM表格可以被压缩后进行查询操作
7. InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁
InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。
例如:
t_user(uid, uname, age, sex) innodb;
uid PK
无其他索引
update t_user set age=10 where uid=1; 命中索引,行锁。
update t_user set age=10 where uid != 1; 未命中索引,表锁。
update t_user set age=10 where name='chackca'; 无索引,表锁。
8、InnoDB表必须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键),而Myisam可以没有
9、Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
如何选择:
1. 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
2. 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB。
3. 系统奔溃后,MyISAM恢复起来更困难,能否接受;
4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
InnoDB为什么推荐使用自增ID作为主键?
答:自增ID可以保证每次插入时B+索引是从右边扩展的,可以避免B+树和频繁合并和分裂(对比使用UUID)。如果使用字符串主键和随机主键,会使得数据随机插入,效率比较差。
innodb引擎的4大特性
插入缓冲(insert buffer),二次写(double write),自适应哈希索引(ahi),预读(read ahead)
事务与死锁
在MySQL的InnoDB中,预设的Tansaction isolation level 为REPEATABLE READ(可重读)
在SELECT 的读取锁定主要分为两种方式:
SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE
这两种方式在事务(Transaction) 进行当中SELECT 到同一个数据表时,都必须等待其它事务数据被提交(Commit)后才会执行。
而主要的不同在于共享锁(lock in share mode) 在有一方事务要Update 同一个表单时很容易造成死锁。
简单的说,如果SELECT 后面若要UPDATE 同一个表单,最好使用SELECT ... UPDATE。
MySQL SELECT ... FOR UPDATE 的Row Lock 与Table Lock
上面介绍过SELECT ... FOR UPDATE 的用法,不过锁定(Lock)的数据是判别就得要注意一下了。由于InnoDB 预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。
举个例子:
假设有个表单products ,里面有id 跟name 二个栏位,id 是主键。
例1: (明确指定主键,并且有此数据,row lock)
SELECT * FROM products WHERE id='3' FOR UPDATE;
例2: (明确指定主键,若查无此数据,无lock)
SELECT * FROM products WHERE id='-1' FOR UPDATE;
例2: (无主键,table lock)
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
例3: (主键不明确,table lock)
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
例4: (主键不明确,table lock)
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
淘宝是如何使用悲观锁的
那么后端的数据库在高并发和超卖下会遇到什么问题呢?主要会有如下3个问题:(主要讨论写的问题,读的问题通过增加cache可以很容易的解决)
I: 首先MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。
II: 其次,超卖的根结在于减库存操作是一个事务操作,需要先select,然后insert,最后update -1。最后这个-1操作是不能出现负数的,但是当多用户在有库存的情况下并发操作,出现负数这是无法避免的。
III:最后,当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。
针对上述问题,如何解决呢? 我们先看眼淘宝的高大上解决方案:
I: 关闭死锁检测,提高并发处理性能。
在一个高并发的MySQL服务器上,事务会递归检测死锁,当超过一定的深度时,性能的下降会变的不可接受。FACEBOOK早就提出了禁止死锁检测。
我们做了一个实验,在禁止死锁检测后,TPS得到了极大的提升,如下图所示:
禁止死锁检测后,即使死锁发生,也不会回滚事务,而是全部等待到超时
Mysql 的 innobase_deadlock_check是在innodb里新加的系统变量,用于控制是否打开死锁检测
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,可以认为如果一个资源被锁定,它总会在以后某个时间被释放。而死锁发生在当多个进程访问同一数据库时,其中每个进程拥有的锁都是其他进程所需的,由此造成每个进程都无法继续下去。
InnoDB的并发写操作会触发死锁,InnoDB也提供了死锁检测机制,可以通过设置innodb_deadlock_detect参数可以打开或关闭死锁检测:
innodb_deadlock_detect = on 打开死锁检测,数据库发生死锁时自动回滚(默认选项)
innodb_deadlock_detect = off 关闭死锁检测,发生死锁的时候,用锁超时来处理,通过设置锁超时参数innodb_lock_wait_timeout 可以在超时发生时回滚被阻塞的事务
设置mysql 事务锁超时时间 innodb_lock_wait_timeout
Mysql数据库采用InnoDB模式,默认参数:innodb_lock_wait_timeout设置锁等待的时间是50s,一旦数据库锁超过这个时间就会报错。
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set (0.00 sec)
mysql> SET GLOBAL innodb_lock_wait_timeout=120;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 120 |
+--------------------------+-------+
1 row in set (0.00 sec)
mysql>
设置InnoDB Monitors方法
还可以通过设置InnDB Monitors来进一步观察锁冲突详细信息
建立test库
mysql>create database test;
Query OK, 1 row affected (0.20 sec)
mysql> use test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> create table innodb_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (1.04 sec)
mysql> create table innodb_tablespace_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.70 sec)
mysql> create table innodb_lock_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.36 sec)
mysql> create table innodb_table_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.08 sec)
可以通过show engine innodb status命令查看死锁信息
mysql> show engine innodb status \G
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2018-05-10 09:17:10 0x7f1fbc21a700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 46 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 53 srv_active, 0 srv_shutdown, 240099 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 2007
OS WAIT ARRAY INFO: signal count 1987
RW-shared spins 3878, rounds 5594, OS waits 1735
RW-excl spins 3, rounds 91, OS waits 4
RW-sx spins 1, rounds 30, OS waits 1
Spin rounds per wait: 1.44 RW-shared, 30.33 RW-excl, 30.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 78405
Purge done for trx's n:o < 78404 undo n:o < 10 state: running but idle
History list length 21
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421249967052640, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
.............................................................................
.............................................................................
.............................................................................
II:请求排队
修改源代码,将排队提到进入引擎层前,降低引擎层面的并发度。
如果请求一股脑的涌入数据库,势必会由于争抢资源造成性能下降,通过排队,让请求从混沌到有序,从而避免数据库在协调大量请求时过载。
请求排队:如果请求一股脑的涌入数据库,势必会由于争抢资源造成性能下降,通过排队,让请求从混沌到有序,从而避免数据库在协调大量请求时过载。
III:请求合并(组提交)
请求合并(组提交),降低server和引擎的交互次数,降低IO消耗。
甲买了一个商品,乙也买了同一个商品,与其把甲乙当做当做单独的请求分别执行一次商品库存减一的操作,不如把他们合并后统一执行一次商品库存减二的操作,请求合并的越多,效率提升的就越大。
实操建议
不过结合我们的实际,死锁监测可以关闭,但是,改mysql源码这种高大上的解决方案显然有那么一点不切实际。
解决方案2:乐观锁
乐观锁
乐观锁并不是真实存在的锁,而是在更新的时候判断此时的库存是否是之前查询出的库存,如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新。类似如下操作:
update tb_sku set stock=2 where id=1 and stock=7;
SKU.objects.filter(id=1, stock=7).update(stock=2)
使用乐观锁需修改数据库的事务隔离级别:
使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)。
修改方式:
MySQL事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
mysql默认的事务隔离级别为repeatable-read
二、事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
三、MySQL事务隔离级别
Mysql默认的事务隔离级别为repeatable-read
四、用例子说明各个隔离级别的情况
1、读未提交:
(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
(5)在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
2、读已提交
(1)打开一个客户端A,并设置当前事务模式为read committed(提交读),查询表account的所有记录:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
(4)客户端B的事务提交
(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3、可重复读
(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
(4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
(5)重新打开客户端B,插入一条新数据后提交
(6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读
4.串行化
(1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+------+--------+---------+
| id | name | balance |
+------+--------+---------+
| 1 | lilei | 10000 |
| 2 | hanmei | 10000 |
| 3 | lucy | 10000 |
| 4 | lily | 10000 |
+------+--------+---------+
4 rows in set (0.00 sec)
(2)打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败,mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(5,'tom',0);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
补充:
1、事务隔离级别为读提交时,写数据只会锁住相应的行
2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
3、事务隔离级别为串行化时,读写数据都会锁住整张表
4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
乐观锁在高并发场景下的问题
乐观锁在高并发场景下的问题,是严重的空自旋
具体可以参考 入大厂必备的基础书籍: 《Java高并发核心编程 卷2》
超卖解决方案3:基于分段的排队执行方案
基于分段的排队执行方案的思想来源
最优的解决方案,其实思想来自于JUC的原理
JUC是如何提高性能的,引入队列
具体可以参考 入大厂必备的基础书籍: 《Java高并发核心编程 卷2》
基于分段的排队执行方案
将提交操作变成两段式,先申请后确认。
申请阶段:
将存库从MySQL前移到Redis中,所有的预减库存的操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。
确认阶段:
然后通过队列等异步手段,将变化的数据异步写入到DB中。
引入队列,然后数据通过队列排序,按照次序更新到DB中,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
基于分段的排队执行方案优点:
解决超卖问题,库存读写都在内存中,故同时解决性能问题。
基于分段的排队执行方案缺点:
由于异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
基于分段的排队执行方案架构图
标签:事务,innodb,最全,索引,死锁,秒杀,InnoDB,超卖,主键 来源: https://www.cnblogs.com/crazymakercircle/p/14846136.html