秒杀超卖 解决方案(史上最全)

文章很长,并且持续更新,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈(总入口) 奉上如下珍贵的学习资源:html


推荐:入大厂 、作架构、大力提高Java 内功 的 精彩博文

入大厂 、作架构、大力提高Java 内功 必备的精彩博文 2021 秋招涨薪1W + 必备的精彩博文
1:Redis 分布式锁 (图解-秒懂-史上最全) 2:Zookeeper 分布式锁 (图解-秒懂-史上最全)
3: Redis与MySQL双写一致性如何保证? (面试必备) 4: 面试必备:秒杀超卖 解决方案 (史上最全)
5:面试必备之:Reactor模式 6: 10分钟看懂, Java NIO 底层原理
7:TCP/IP(图解+秒懂+史上最全) 8:Feign原理 (图解)
9:DNS图解(秒懂 + 史上最全 + 高薪必备) 10:CDN图解(秒懂 + 史上最全 + 高薪必备)
10: 分布式事务( 图解 + 史上最全 + 吐血推荐 )

Java 面试题 30个专题 , 史上最全 , 面试必刷 阿里、京东、美团... 随意挑、横着走!!!
1: JVM面试题(史上最强、持续更新、吐血推荐) 2:Java基础面试题(史上最全、持续更新、吐血推荐
3:架构设计面试题 (史上最全、持续更新、吐血推荐) 4:设计模式面试题 (史上最全、持续更新、吐血推荐)
1七、分布式事务面试题 (史上最全、持续更新、吐血推荐) 一致性协议 (史上最全)
2九、多线程面试题(史上最全) 30、HR面经,过五关斩六将后,当心阴沟翻船!
9.网络协议面试题(史上最全、持续更新、吐血推荐) 更多专题, 请参见【 疯狂创客圈 高并发 总目录

SpringCloud 精彩博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
SpringCloud gateway (史上最全) 更多专题, 请参见【 疯狂创客圈 高并发 总目录

前言

先来就库存超卖的问题做描述:通常电子商务网站都会遇到如团购、秒杀、特价之类的活动,而这样的活动有一个共同的特色就是访问量激增、上千甚至上万人抢购一个商品。然而,做为活动商品,库存确定是颇有限的,如何控制库存不让出现超买,以防止形成没必要要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。前端

在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每一个抢购活动都要面临的难点。java

1、问题描述

在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,致使库存的最终结果出现异常。问题:
当商品A一共有库存15件,用户甲先下单10件,用户乙下单8件,这时候库存只能知足一我的下单成功,若是两我的同时提交,就出现了超卖的问题。python

在这里插入图片描述

2、解决的三种方案

  • 悲观锁

经过悲观锁解决超卖mysql

  • 乐观锁

经过乐观锁解决超卖程序员

  • 分段执行的排队方案

经过分段执行的排队方案解决超卖面试

解决方案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(提交事务)

问题:sql

注意,上面的代码容易出现死锁,采用很少。数据库

有社群小伙伴,对死锁的的缘由比较关心,这里简单分析一下。

上面的语句,可能出现死锁的简单的缘由,在事务的隔离级别为Serializable时,假设事务t1经过 select拿到了共享锁,而其余事务若是拿到了 排他锁,此时t1 去拿排他锁的时候, 就有可能会出现死锁,注意,这里是可能,并非必定。实际的缘由,与事务的隔离级别和语句的复杂度,都有关系。

总之,避免死锁的方式之一(稍后介绍):为了在单个 InnoDB 表上执行多个并发写入操做时避免死锁,能够在事务开始时经过为预期要修改的每一个元祖(行)使用 SELECT … FOR UPDATE 语句来获取必要的锁,即便这些行的更改语句是在以后才执行的。

解决方案:通常提早采用 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 的区别

  1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,因此最好把多条SQL语言放在begin和commit之间,组成一个事务;

  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;

  3. 汇集索引 VS 非汇集索引

    InnoDB是汇集索引,使用B+Tree做为索引结构,数据文件是和(主键)索引绑在一块儿的(表数据文件自己就是按B+Tree组织的一个索引结构),必需要有主键,经过主键索引效率很高。可是辅助索引须要两次查询,先查询到主键,而后再经过主键查询到数据。所以,主键不该该过大,由于主键太大,其余索引也都会很大。

InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值

img

but, MyISAM是非汇集索引,也是使用B+Tree做为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

img

总结

​ 也就是说:InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。

  1. InnoDB不保存表的具体行数,执行select count(*) from table时须要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只须要读出该变量便可,速度很快(注意不能加有任何WHERE条件);

那么为何InnoDB没有了这个变量呢?

​ 由于InnoDB的事务特性,在同一时刻表中的行数对于不一样的事务而言是不同的,所以count统计会计算对于当前事务而言能够统计到的行数,而不是将总行数储存起来方便快速查询。InnoDB会尝试遍历一个尽量小的索引除非优化器提示使用别的索引。若是二级索引不存在,InnoDB还会尝试去遍历其余聚簇索引。
​ 若是索引并无彻底处于InnoDB维护的缓冲区(Buffer Pool)中,count操做会比较费时。能够创建一个记录总行数的表并让你的程序在INSERT/DELETE时更新对应的数据。和上面提到的问题同样,若是此时存在多个事务的话这种方案也不太好用。若是获得大体的行数值已经足够知足需求能够尝试SHOW TABLE STATUS

  1. Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引领域的查询效率上MyISAM速度更快高;PS:5.7之后的InnoDB支持全文索引了

  2. MyISAM表格能够被压缩后进行查询操做

  3. 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';    无索引,表锁。

八、InnoDB表必须有惟一索引(如主键)(用户没有指定的话会本身找/生产一个隐藏列Row_id来充当默认主键),而Myisam能够没有

九、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获得了极大的提高,以下图所示:

img

禁止死锁检测后,即便死锁发生,也不会回滚事务,而是所有等待到超时

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源码这种高大上的解决方案显然有那么一点不切实际。

InnoDB锁定模式及实现机制

考虑到行级锁定均由各个存储引擎自行实现,并且具体实现也各有差异,而InnoDB是目前事务型存储引擎中使用最为普遍的存储引擎,因此这里咱们就主要分析一下InnoDB的锁定特性。
总的来讲,InnoDB的锁定机制和Oracle数据库有很多类似之处。InnoDB的行级锁定一样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程当中为了让行级锁定和表级锁定共存,InnoDB也一样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。
当一个事务须要给本身须要的某个资源加锁的时候,若是遇到一个共享锁正锁定着本身须要的资源的时候,本身能够再加一个共享锁,不过不能加排他锁。可是,若是遇到本身须要锁定的资源已经被一个排他锁占有以后,则只能等待该锁定释放资源以后本身才能获取锁定资源并添加本身的锁定。而意向锁的做用就是当一个事务在须要获取资源锁定的时候,若是遇到本身须要的资源已经被排他锁占用的时候,该事务能够须要锁定行的表上面添加一个合适的意向锁。若是本身须要一个共享锁,那么就在表上面添加一个意向共享锁。而若是本身须要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁能够同时并存多个,可是意向排他锁同时只能有一个存在。

InnoDB的锁定模式实际上能够分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX),咱们能够经过如下表格来总结上面这四种所的共存逻辑关系:
img

若是一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,若是二者不兼容,该事务就要等待锁释放。

意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务能够经过如下语句显示给记录集加共享锁或排他锁。

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE

用SELECT ... IN SHARE MODE得到共享锁,主要用在须要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操做。

可是若是当前事务也须要对该记录进行更新操做,则颇有可能形成死锁,对于锁定行记录后须要进行更新操做的应用,应该使用SELECT... FOR UPDATE方式得到排他锁。

间隙锁(Next-Key锁)

当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
例:
假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:

mysql> select * from emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不只会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的:
(1)防止幻读,以知足相关隔离级别的要求。对于上面的例子,要是不使用间隙锁,若是其余事务插入了empid大于100的任何记录,那么本事务若是再次执行上述语句,就会发生幻读;
(2)为了知足其恢复和复制的须要。
很显然,在使用范围条件检索并锁定记录时,即便某些不存在的键值也会被无辜的锁定,而形成在锁定的时候没法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能形成很大的危害。
除了间隙锁给InnoDB带来性能的负面影响以外,经过索引实现锁定的方式还存在其余几个较大的性能隐患:
(1)当Query没法利用索引的时候,InnoDB会放弃使用行级别锁定而改用表级别的锁定,形成并发性能的下降;
(2)当Query使用的索引并不包含全部过滤条件的时候,数据检索使用到的索引键所只想的数据可能有部分并不属于该Query的结果集的行列,可是也会被锁定,由于间隙锁锁定的是一个范围,而不是具体的索引键;
(3)当Query在使用索引定位数据的时候,若是使用的索引键同样但访问的数据行不一样的时候(索引只是过滤条件的一部分),同样会被锁定。
所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了经过范围条件加锁时使用间隙锁外,若是使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁。

并发事务有什么什么问题?应该如何解决?

并发事务可能形成:脏读、不可重复读和幻读等问题 ,这些问题其实都是数据库读一致性问题,必须由数据库提供必定的事务隔离机制来解决,解决方案以下:

  • 加锁:在读取数据前,对其加锁,阻止其余事务对数据进行修改。
  • 提供数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 经过必定机制生成一个数据请求时间点的一致性数据快照(Snapshot), 并用这个快照来提供必定级别 (语句级或事务级) 的一致性读取,从用户的角度来看,好象是数据库能够提供同一数据的多个版本。

什么是 MVCC?

MVCC 全称是多版本并发控制系统,InnoDB 和 Falcon 存储引擎经过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题。

MVCC 是怎么工做的?

InnoDB 的 MVCC 是经过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的建立时间,一个保存行的过时时间(删除时间)。固然存储的并非真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增,事务开始时刻的系统版本号会做为事务的版本号,用来查询到每行记录的版本号进行比较。

REPEATABLE READ(可重读)隔离级别下 MVCC 如何工做?

  • SELECT:InnoDB 会根据如下条件检查每一行记录:第一,InnoDB 只查找版本早于当前事务版本的数据行,这样能够确保事务读取的行要么是在开始事务以前已经存在要么是事务自身插入或者修改过的。第二,行的删除版本号要么未定义,要么大于当前事务版本号,这样能够确保事务读取到的行在事务开始以前未被删除。
  • INSERT:InnoDB 为新插入的每一行保存当前系统版本号做为行版本号。
  • DELETE:InnoDB 为删除的每一行保存当前系统版本号做为行删除标识。
  • UPDATE:InnoDB 为插入的一行新纪录保存当前系统版本号做为行版本号,同时保存当前系统版本号到原来的行做为删除标识保存这两个版本号,使大多数操做都不用加锁。它不足之处是每行记录都须要额外的存储空间,须要作更多的行检查工做和一些额外的维护工做。

快照读和当前读

在mysql中select分为快照读和当前读,执行下面的语句

select * from table where id = ?;
执行的是快照读,读的是数据库记录的快照版本,是不加锁的。(这种说法在隔离级别为Serializable中不成立)

select加锁分析

下面六句Sql的区别呢

select * from table where id = ?
select * from table where id < ?
select * from table where id = ? lock in share mode
select * from table where id < ? lock in share mode
select * from table where id = ? for update
select * from table where id < ? for update

在不一样的事务隔离级别下,是否加锁,加的是共享锁仍是排他锁,是否存在间隙锁,您能说出来嘛?
要回答这个问题,先问本身三个问题

  • 当前事务隔离级别是什么
  • id列是否存在索引
  • 若是存在索引是聚簇索引仍是非聚簇索引呢?

关于mysql的索引,啰嗦一下:

  • innodb必定存在聚簇索引,默认以主键做为聚簇索引
  • 有几个索引,就有几棵B+树(不考虑hash索引的情形)
  • 聚簇索引的叶子节点为磁盘上的真实数据。非聚簇索引的叶子节点仍是索引,指向聚簇索引B+树。

锁类型

  • 共享锁(S锁):假设事务T1对数据A加上共享锁,那么事务T2能够读数据A,不能修改数据A。
  • 排他锁(X锁):假设事务T1对数据A加上共享锁,那么事务T2不能读数据A,不能修改数据A。
    咱们经过update、delete等语句加上的锁都是行级别的锁。只有LOCK TABLE … READ和LOCK TABLE … WRITE才能申请表级别的锁。
  • 意向共享锁(IS锁):一个事务在获取(任何一行/或者全表)S锁以前,必定会先在所在的表上加IS锁。
  • 意向排他锁(IX锁):一个事务在获取(任何一行/或者全表)X锁以前,必定会先在所在的表上加IX锁。

意向锁存在的目的?

这里说一下意向锁存在的目的。假设事务T1,用X锁来锁住了表上的几条记录,那么此时表上存在IX锁,即意向排他锁。那么此时事务T2要进行LOCK TABLE … WRITE的表级别锁的请求,能够直接根据意向锁是否存在而判断是否有锁冲突。

  • Record Locks:简单翻译为行锁吧。注意了,该锁是对索引记录进行加锁!锁是在加索引上而不是行上的。注意了,innodb必定存在聚簇索引,所以行锁最终都会落到聚簇索引上!
  • Gap Locks:简单翻译为间隙锁,是对索引的间隙加锁,其目的只有一个,防止其余事物插入数据。在Read Committed隔离级别下,不会使用间隙锁。

这里我对官网补充一下,隔离级别比Read Committed低的状况下,也不会使用间隙锁,如隔离级别为Read Uncommited时,也不存在间隙锁。当隔离级别为Repeatable Read和Serializable时,就会存在间隙锁。

  • Next-Key Locks:这个理解为Record Lock + 索引前面的Gap Lock。记住了,锁住的是索引前面的间隙!好比一个索引包含值,10,11,13和20。那么,间隙锁的范围以下

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

索引原理介绍

先来一张带主键的表,以下所示,pId是主键

pId name birthday
5 zhangsan 2016-10-02
8 lisi 2015-10-04
11 wangwu 2016-09-02
13 zhaoliu 2015-10-07

画出该表的结构图以下
image

如上图所示,分为上下两个部分,上半部分是由主键造成的B+树,下半部分就是磁盘上真实的数据!那么,当咱们, 执行下面的语句

select * from table where pId='11'

那么,执行过程以下
image
如上图所示,从根开始,通过3次查找,就能够找到真实数据。若是不使用索引,那就要在磁盘上,进行逐行扫描,直到找到数据位置。显然,使用索引速度会快。可是在写入数据的时候,须要维护这颗B+树的结构,所以写入性能会降低!

聚簇索引、非聚簇索引

聚簇索引:将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据

非聚簇索引:将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置

在innodb中,在聚簇索引之上建立的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、惟一索引。辅助索引叶子节点存储的再也不是行的物理位置,而是主键值,辅助索引访问数据老是须要二次查找

img

  1. InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法便可查找到对应的叶节点,以后得到行数据。
  2. 若对Name列进行条件搜索,则须要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操做,最终到达叶子节点便可获取整行数据。(重点在于经过其余键须要创建辅助索引)

聚簇索引具备惟一性,因为聚簇索引是将数据跟索引结构放到一块,所以一个表仅有一个聚簇索引。

表中行的物理顺序和索引中行的物理顺序是相同的在建立任何非聚簇索引以前建立聚簇索引,这是由于聚簇索引改变了表中行的物理顺序,数据行 按照必定的顺序排列,而且自动维护这个顺序;

聚簇索引默认是主键,若是表中没有定义主键,InnoDB 会选择一个惟一且非空的索引代替。若是没有这样的索引,InnoDB 会隐式定义一个主键(相似oracle中的RowId)来做为聚簇索引。若是已经设置了主键为聚簇索引又但愿再单独设置聚簇索引,必须先删除主键,而后添加咱们想要的聚簇索引,最后恢复设置主键便可。

MyISAM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不一样,节点的结构彻底一致只是存储的内容不一样而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来讲,这两个键没有任何差异。因为索引树是独立的,经过辅助键检索无需访问主键的索引树

img

使用聚簇索引的优点:

每次使用辅助索引检索都要通过两次B+树查找,看上去聚簇索引的效率明显要低于非聚簇索引,这不是画蛇添足吗?聚簇索引的优点在哪?

1.因为行数据和聚簇索引的叶子节点存储在一块儿,同一页中会有多条行数据,访问同一数据页不一样行记录时,已经把页加载到了Buffer中(缓存器),再次访问时,会在内存中完成访问,没必要访问磁盘。这样主键和行数据是一块儿被载入内存的,找到叶子节点就能够马上将行数据返回了,若是按照主键Id来组织数据,得到数据更快。

2.辅助索引的叶子节点,存储主键值,而不是数据的存放地址。好处是当行数据放生变化时,索引树的节点也须要分裂变化;或者是咱们须要查找的数据,在上一次IO读写的缓存中没有,须要发生一次新的IO操做时,能够避免对辅助索引的维护工做,只须要维护聚簇索引树就行了。另外一个好处是,由于辅助索引存放的是主键值,减小了辅助索引占用的存储空间大小。

注:咱们知道一次io读写,能够获取到16K大小的资源,咱们称之为读取到的数据区域为Page。而咱们的B树,B+树的索引结构,叶子节点上存放好多个关键字(索引值)和对应的数据,都会在一次IO操做中被读取到缓存中,因此在访问同一个页中的不一样记录时,会在内存里操做,而不用再次进行IO操做了。除非发生了页的分裂,即要查询的行数据不在上次IO操做的换村里,才会触发新的IO操做。

3.由于MyISAM的主索引并不是聚簇索引,那么他的数据的物理地址必然是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,因而开始不停的寻道不停的旋转。聚簇索引则只需一次I/O。(强烈的对比)

4.不过,若是涉及到大数据量的排序、全表扫描、count之类的操做的话,仍是MyISAM占优点些,由于索引所占空间小,这些操做是须要在内存中完成的。

聚簇索引须要注意的地方

当使用主键为聚簇索引时,主键最好不要使用uuid,由于uuid的值太过离散,不适合排序且可能出线新增长记录的uuid,会插入在索引树中间的位置,致使索引树调整复杂度变大,消耗更多的时间和资源。

建议使用int类型的自增,方便排序而且默认会在索引树的末尾增长主键值,对索引树的结构影响最小。并且,主键值占用的存储空间越大,辅助索引中保存的主键值也会跟着变大,占用存储空间,也会影响到IO操做读取到的数据量。

为何主键一般建议使用自增id

聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据必定也是相邻地存放在磁盘上的。若是主键不是自增id,那么能够想 象,它会干些什么,不断地调整数据的物理地址、分页,固然也有其余一些措施来减小这些操做,但却没法完全避免。但,若是是自增的,那就简单了,它只须要一 页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。

四个隔离级别

咱们先回忆一下事务的四个隔离级别,他们由弱到强以下所示:

  • Read Uncommited(RU):读未提交,一个事务能够读到另外一个事务未提交的数据!
  • Read Committed (RC):读已提交,一个事务能够读到另外一个事务已提交的数据!
  • Repeatable Read:(RR):可重复读,加入间隙锁,必定程度上避免了幻读的产生!注意了,只是必定程度上,并无彻底避免!我会在下一篇文章说明!另外就是记住从该级别才开始加入间隙锁(这句话记下来,后面有用到)!
  • Serializable:串行化,该级别下读写串行化,且全部的select语句后都自动加上lock in share mode,即便用了共享锁。所以在该隔离级别下,使用的是当前读,而不是快照读。

select 分析的表数据

为了便于说明,我来个例子,假设有表数据以下,pId为主键索引

pId(int) name(varchar) num(int)
1 aaa 100
2 bbb 200
7 ccc 200

隔离级别:RC/RU ,条件列: 非索引

(1)select * from table where num = 200
不加任何锁,是快照读。
(2)select * from table where num > 200
不加任何锁,是快照读。
(3)select * from table where num = 200 lock in share mode
当num = 200,有两条记录。这两条记录对应的pId=2,7,所以在pId=2,7的聚簇索引上加行级S锁,采用当前读。
(4)select * from table where num > 200 lock in share mode
当num > 200,有一条记录。这条记录对应的pId=3,所以在pId=3的聚簇索引上加上行级S锁,采用当前读。
(5)select * from table where num = 200 for update
当num = 200,有两条记录。这两条记录对应的pId=2,7,所以在pId=2,7的聚簇索引上加行级X锁,采用当前读。
(6)select * from table where num > 200 for update
当num > 200,有一条记录。这条记录对应的pId=3,所以在pId=3的聚簇索引上加上行级X锁,采用当前读。

隔离级别:RC/RU ,条件列: 聚簇索引

你们应该知道pId是主键列,所以pId用的就是聚簇索引。此状况其实和RC/RU+条件列非索引状况是相似的。
(1)select * from table where pId = 2
不加任何锁,是快照读。
(2)select * from table where pId > 2
不加任何锁,是快照读。
(3)select * from table where pId = 2 lock in share mode
在pId=2的聚簇索引上,加S锁,为当前读。
(4)select * from table where pId > 2 lock in share mode
在pId=3,7的聚簇索引上,加S锁,为当前读。
(5)select * from table where pId = 2 for update
在pId=2的聚簇索引上,加X锁,为当前读。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上,加X锁,为当前读。

为何条件列加不加索引,加锁状况是同样的?

实际上是不同的。在RC/RU隔离级别中,MySQL作了优化。在条件列没有索引的状况下,尽管经过聚簇索引来扫描全表,进行全表加锁。可是,MySQL Server层会进行过滤并把不符合条件的锁立即释放掉,所以你看起来最终结果是同样的。可是RC/RU+条件列非索引比本例多了一个释放不符合条件的锁的过程!

隔离级别:RC/RU ,条件列: 非聚簇索引

在num列上建上非惟一索引。此时有一棵聚簇索引(主键索引,pId)造成的B+索引树,其叶子节点为硬盘上的真实数据。以及另外一棵非聚簇索引(非惟一索引,num)造成的B+索引树,其叶子节点依然为索引节点,保存了num列的字段值,和对应的聚簇索引。

(1)select * from table where num = 200
不加任何锁,是快照读。
(2)select * from table where num > 200
不加任何锁,是快照读。
(3)select * from table where num = 200 lock in share mode
当num = 200,因为num列上有索引,所以先在 num = 200的两条索引记录上加行级S锁。接着,去聚簇索引树上查询,这两条记录对应的pId=2,7,所以在pId=2,7的聚簇索引上加行级S锁,采用当前读。
(4)select * from table where num > 200 lock in share mode
当num > 200,因为num列上有索引,所以先在符合条件的 num = 300的一条索引记录上加行级S锁。接着,去聚簇索引树上查询,这条记录对应的pId=3,所以在pId=3的聚簇索引上加行级S锁,采用当前读。
(5)select * from table where num = 200 for update
当num = 200,因为num列上有索引,所以先在 num = 200的两条索引记录上加行级X锁。接着,去聚簇索引树上查询,这两条记录对应的pId=2,7,所以在pId=2,7的聚簇索引上加行级X锁,采用当前读。
(6)select * from table where num > 200 for update
当num > 200,因为num列上有索引,所以先在符合条件的 num = 300的一条索引记录上加行级X锁。接着,去聚簇索引树上查询,这条记录对应的pId=3,所以在pId=3的聚簇索引上加行级X锁,采用当前读。

隔离级别:RR/Serializable,条件列: 非索引

RR级别须要多考虑的就是gap lock,他的加锁特征在于,不管你怎么查都是锁全表。接下来分析开始
(1)select * from table where num = 200
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,在pId = 1,2,3,7(全表全部记录)的聚簇索引上加S锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(2)select * from table where num > 200
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,在pId = 1,2,3,7(全表全部记录)的聚簇索引上加S锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(3)select * from table where num = 200 lock in share mode
在pId = 1,2,3,7(全表全部记录)的聚簇索引上加S锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(4)select * from table where num > 200 lock in share mode
在pId = 1,2,3,7(全表全部记录)的聚簇索引上加S锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(5)select * from table where num = 200 for update
在pId = 1,2,3,7(全表全部记录)的聚簇索引上加X锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(6)select * from table where num > 200 for update
在pId = 1,2,3,7(全表全部记录)的聚簇索引上加X锁。而且在
聚簇索引的全部间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock

隔离级别:RR/Serializable,条件列: 聚簇索引

你们应该知道pId是主键列,所以pId用的就是聚簇索引。该状况的加锁特征在于,若是where后的条件为精确查询(=的状况),那么只存在record lock。若是where后的条件为范围查询(>或<的状况),那么存在的是record lock+gap lock。
(1)select * from table where pId = 2
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,是当前读,在pId=2的聚簇索引上加S锁,不存在gap lock。
(2)select * from table where pId > 2
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,是当前读,在pId=3,7的聚簇索引上加S锁。在(2,3)(3,7)(7,+∞)加上gap lock
(3)select * from table where pId = 2 lock in share mode
是当前读,在pId=2的聚簇索引上加S锁,不存在gap lock。
(4)select * from table where pId > 2 lock in share mode
是当前读,在pId=3,7的聚簇索引上加S锁。在(2,3)(3,7)(7,+∞)加上gap lock
(5)select * from table where pId = 2 for update
是当前读,在pId=2的聚簇索引上加X锁。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上加X锁。在(2,3)(3,7)(7,+∞)加上gap lock
(7)select * from table where pId = 6 [lock in share mode|for update]
注意了,pId=6是不存在的列,这种状况会在(3,7)上加gap lock。
(8)select * from table where pId > 18 [lock in share mode|for update]
注意了,pId>18,查询结果是空的。在这种状况下,是在(7,+∞)上加gap lock。

隔离级别:RR/Serializable,条件列: 非聚簇索引

这里非聚簇索引,须要区分是否为惟一索引。由于若是是非惟一索引,间隙锁的加锁方式是有区别的。
先说一下,惟一索引的状况。若是是惟一索引,状况和RR/Serializable+条件列是聚簇索引相似,惟一有区别的是:这个时候有两棵索引树,加锁是加在对应的非聚簇索引树和聚簇索引树上!你们能够自行推敲!
下面说一下,非聚簇索引是非惟一索引的状况,他和惟一索引的区别就是经过索引进行精确查询之后,不只存在record lock,还存在gap lock。而经过惟一索引进行精确查询后,只存在record lock,不存在gap lock。老规矩在num列创建非惟一索引
(1)select * from table where num = 200
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非汇集索引上加S锁,在(100,200)(200,300)加上gap lock。
(2)select * from table where num > 200
在RR级别下,不加任何锁,是快照读。
在Serializable级别下,是当前读,在pId=3的聚簇索引上加S锁,在num=300的非汇集索引上加S锁。在(200,300)(300,+∞)加上gap lock
(3)select * from table where num = 200 lock in share mode
是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非汇集索引上加S锁,在(100,200)(200,300)加上gap lock。
(4)select * from table where num > 200 lock in share mode
是当前读,在pId=3的聚簇索引上加S锁,在num=300的非汇集索引上加S锁。在(200,300)(300,+∞)加上gap lock。
(5)select * from table where num = 200 for update
是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非汇集索引上加X锁,在(100,200)(200,300)加上gap lock。
(6)select * from table where num > 200 for update
是当前读,在pId=3的聚簇索引上加S锁,在num=300的非汇集索引上加X锁。在(200,300)(300,+∞)加上gap lock
(7)select * from table where num = 250 [lock in share mode|for update]
注意了,num=250是不存在的列,这种状况会在(200,300)上加gap lock。
(8)select * from table where num > 400 [lock in share mode|for update]
注意了,pId>400,查询结果是空的。在这种状况下,是在(400,+∞)上加gap lock。

死锁

MyISAM表锁是deadlock free的,这是由于MyISAM老是一次得到所需的所有锁,要么所有知足,要么等待,所以不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步得到的,当两个事务都须要得到对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。

如何避免死锁?

  • 为了在单个 InnoDB 表上执行多个并发写入操做时避免死锁,能够在事务开始时经过为预期要修改的每一个元祖(行)使用 SELECT … FOR UPDATE 语句来获取必要的锁,即便这些行的更改语句是在以后才执行的。

  • 在事务中,若是要更新记录,应该直接申请足够级别的锁,即排他锁,而不该先申请共享锁、更新时再申请排他锁,由于这时候当用户再申请排他锁时,其余事务可能又已经得到了相同记录的共享锁,从而形成锁冲突,甚至死锁

  • 若是事务须要修改或锁定多个表,则应在每一个事务中以相同的顺序使用加锁语句。在应用中,若是不一样的程序会并发存取多个表,应尽可能约定以相同的顺序来访问表,这样能够大大下降产生死锁的机会

  • 在程序以批量方式处理数据的时候,若是事先对数据排序,保证每一个线程按固定的顺序来处理记录,也能够大大下降出现死锁的可能。

  • 在REPEATABLE-READ隔离级别下,若是两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录状况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,若是两个线程都这么作,就会出现死锁。这种状况下,将隔离级别改为READ COMMITTED,就可避免问题。

  • 当隔离级别为READ COMMITTED时,若是两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,若是没有,就插入记录。此时,只有一个线程能插入成功,另外一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会得到一个排他锁。这时若是有第3个线程又来申请排他锁,也会出现死锁。对于这种状况,能够直接作插入操做,而后再捕获主键重异常,或者在遇到主键重错误时,老是执行ROLLBACK释放得到的排他锁

InnoDB 默认是如何对待死锁的?

InnoDB 默认是使用设置死锁时间来让死锁超时的策略,默认 innodblockwait_timeout 设置的时长是 50s。

如何开启死锁检测?

设置 innodbdeadlockdetect 设置为 on 能够主动检测死锁,在 Innodb 中这个值默认就是 on 开启的状态。

解决方案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

img

并发事务会带来哪些问题?

  一、脏读:事务A读取了事务B更新的数据,而后B回滚操做,那么A读取到的数据是脏数据

  二、不可重复读:事务 A 屡次读取同一数据,事务 B 在事务A屡次读取的过程当中,对数据做了更新并提交,致使事务A屡次读取同一数据时,结果 不一致。

  三、幻读:系统管理员A将数据库中全部学生的成绩从具体分数改成ABCDE等级,可是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉同样,这就叫幻读。

  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住知足条件的行,解决幻读须要锁表

3、MySQL事务隔离级别

img

Mysql默认的事务隔离级别为repeatable-read

img

4、用例子说明各个隔离级别的状况

一、读未提交:

(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:

img

 (2)在客户端A的事务提交以前,打开另外一个客户端B,更新表account:

img

 (3)这时,虽然客户端B的事务还没提交,可是客户端A就能够查询到B已经更新的数据:

img

(4)一旦客户端B的事务由于某种缘由回滚,全部的操做都将会被撤销,那客户端A查询到的数据其实就是脏数据:

img

(5)在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,竟然是400,是否是很奇怪,数据不一致啊,若是你这么想就太天真 了,在应用程序中,咱们会用400-50=350,并不知道其余会话回滚了,要想解决这个问题能够采用读已提交的隔离级别

img

 二、读已提交

(1)打开一个客户端A,并设置当前事务模式为read committed(提交读),查询表account的全部记录:

img

 (2)在客户端A的事务提交以前,打开另外一个客户端B,更新表account:

img

(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:

img

(4)客户端B的事务提交

img

(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题

img

  三、可重复读

(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的全部记录

img

(2)在客户端A的事务提交以前,打开另外一个客户端B,更新表account并提交

img

(3)在客户端A查询表account的全部记录,与步骤(1)查询结果一致,没有出现不可重复读的问题

img

(4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,因此是300,数据的一致性却是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操做不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。

img

(5)从新打开客户端B,插入一条新数据后提交

img

(6)在客户端A查询表account的全部记录,没有 查出 新增数据,因此没有出现幻读

img

 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

  

  补充:

  一、事务隔离级别为读提交时,写数据只会锁住相应的行

  二、事务隔离级别为可重复读时,若是检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;若是检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其余事务是不能在这个间隙插入记录的,这样能够防止幻读。

  三、事务隔离级别为串行化时,读写数据都会锁住整张表

   四、隔离级别越高,越能保证数据的完整性和一致性,可是对并发性能的影响也越大。

乐观锁在高并发场景下的问题

乐观锁在高并发场景下的问题,是严重的空自旋

具体能够参考 入大厂必备的基础书籍: 《Java高并发核心编程 卷2》

超卖解决方案3:分阶段排队下单方案

分阶段排队下单方案的思想来源

最优的解决方案,其实思想来自于JUC的原理

JUC是如何提升性能的,引入队列

原始的CLH队列

用于减小线程争用的最简单的队列,CLH队列,具体能够参考 入大厂必备的基础书籍: 《Java高并发核心编程 卷2》

在这里插入图片描述

JUC的AQS内部队列

AQS内部队列是JUC高性能的基础,AQS队列,具体能够参考 入大厂必备的基础书籍: 《Java高并发核心编程 卷2》

在这里插入图片描述

分阶段排队下单方案

将提交操做变成两段式:

  • 第一阶段申请,申请预减减库,申请成功以后,进入消息队列;

  • 第二阶段确认,从消息队列消费申请令牌,而后完成下单操做。 查库存 -> 建立订单 -> 扣减库存。经过分布式锁保障解决多个provider实例并发下单产生的超卖问题。

申请阶段:

将存库从MySQL前移到Redis中,全部的预减库存的操做放到内存中,因为Redis中不存在锁故不会出现互相等待,而且因为Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。

确认阶段:

而后经过队列等异步手段,将变化的数据异步写入到DB中。

引入队列,而后数据经过队列排序,按照次序更新到DB中,彻底串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。

分阶段排队架构图

图解削峰限流技术RabbitMq 消息队列解决高并发高并发下削峰限流

基于分段的排队执行方案的性能提高

一个高性能秒杀的场景:

假设一个商品1分钟6000订单,每秒的 600个下单操做。

在排队阶段,每秒的 600个预减库存的操做,对于 Redis 来讲,没有任何压力。甚至每秒的 6000个预减库存的操做,对于 Redis 来讲,也是压力不大。

可是在下单阶段,就不同了。假设加锁以后,释放锁以前,查库存 -> 建立订单 -> 扣减库存,通过优化,每一个IO操做100ms,大概200毫秒,一秒钟5个订单。600个订单须要120s,2分钟才能完全消化。

如何提高下单阶段的性能呢?

在这里插入图片描述

可使用Redis 分段锁。

为了达到每秒600个订单,能够将锁分红 600 /5 =120 个段,每一个段负责5个订单,600个订单,在第二个阶段1秒钟下单完成。

在这里插入图片描述

有关Redis分段锁的详细知识,请阅读下面的博文:

Redis分布式锁 (图解-秒懂-史上最全)

基于分段的排队执行方案优势:

解决超卖问题,库存读写都在内存中,故同时解决性能问题。

基于分段的排队执行方案缺点:

  • 数据不一致的问题:

因为异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。

  • 可能存在少买

可能存在少买,也就是若是拿到号的人不真正下订单,可能库存减为0,可是订单数并无达到库存阀值。

参考文献:

http://www.javashuo.com/article/p-resvyvjh-ch.html

http://www.javashuo.com/article/p-bjxxhkyq-dx.html

https://www.cnblogs.com/wyaokai/p/10921323.html

相关文章
相关标签/搜索