-
mysql处理高并发,防止库存超卖-转
2017-09-13 20:44:09
今天王总又给我们上了一课,其实mysql处理高并发,防止库存超卖的问题,在去年的时候,王总已经提过;但是很可惜,即使当时大家都听懂了,但是在现实开发中,还是没这方面的意识。今天就我的一些理解,整理一下这个问题,并希望以后这样的课程能多点。
先来就库存超卖的问题作描述:一般电子商务网站都会遇到如团购、秒杀、特价之类的活动,而这样的活动有一个共同的特点就是访问量激增、上千甚至上万人抢购一个商品。然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。
从技术方面剖析,很多人肯定会想到事务,但是事务是控制库存超卖的必要条件,但不是充分必要条件。
举例:
总库存:4个商品
请求人:a、1个商品 b、2个商品 c、3个商品
程序如下:
beginTranse(开启事务)
try{
$result = $dbca->query('select amount from s_store where postID = 12345');
if(result->amount > 0){
//quantity为请求减掉的库存数量
$dbca->query('update s_store set amount = amount - quantity where postID = 12345');
}
}catch($e Exception){
rollBack(回滚)
}
commit(提交事务)
以上代码就是我们平时控制库存写的代码了,大多数人都会这么写,看似问题不大,其实隐藏着巨大的漏洞。数据库的访问其实就是对磁盘文件的访问,数据库中的表其实就是保存在磁盘上的一个个文件,甚至一个文件包含了多张表。例如由于高并发,当前有三个用户a、b、c三个用户进入到了这个事务中,这个时候会产生一个共享锁,所以在select的时候,这三个用户查到的库存数量都是4个,同时还要注意,mysql innodb查到的结果是有版本控制的,再其他用户更新没有commit之前(也就是没有产生新版本之前),当前用户查到的结果依然是就版本;
然后是update,假如这三个用户同时到达update这里,这个时候update更新语句会把并发串行化,也就是给同时到达这里的是三个用户排个序,一个一个执行,并生成排他锁,在当前这个update语句commit之前,其他用户等待执行,commit后,生成新的版本;这样执行完后,库存肯定为负数了。但是根据以上描述,我们修改一下代码就不会出现超买现象了,代码如下:
beginTranse(开启事务)
try{
//quantity为请求减掉的库存数量
$dbca->query('update s_store set amount = amount - quantity where postID = 12345');$result = $dbca->query('select amount from s_store where postID = 12345');
if(result->amount < 0){
throw new Exception('库存不足');
}
}catch($e Exception){
rollBack(回滚)
}
commit(提交事务)
另外,更简洁的方法:
beginTranse(开启事务)
try{
//quantity为请求减掉的库存数量
$dbca->query('update s_store set amount = amount - quantity where amount>=quantity and postID = 12345');}catch($e Exception){
rollBack(回滚)
}
commit(提交事务)
=====================================================================================
1、在秒杀的情况下,肯定不能如此高频率的去读写数据库,会严重造成性能问题的
必须使用缓存,将需要秒杀的商品放入缓存中,并使用锁来处理其并发情况。当接到用户秒杀提交订单的情况下,先将商品数量递减(加锁/解锁)后再进行其他方面的处理,处理失败在将数据递增1(加锁/解锁),否则表示交易成功。
当商品数量递减到0时,表示商品秒杀完毕,拒绝其他用户的请求。2、这个肯定不能直接操作数据库的,会挂的。直接读库写库对数据库压力太大,要用缓存。
把你要卖出的商品比如10个商品放到缓存中;然后在memcache里设置一个计数器来记录请求数,这个请求书你可以以你要秒杀卖出的商品数为基数,比如你想卖出10个商品,只允许100个请求进来。那当计数器达到100的时候,后面进来的就显示秒杀结束,这样可以减轻你的服务器的压力。然后根据这100个请求,先付款的先得后付款的提示商品以秒杀完。3、首先,多用户并发修改同一条记录时,肯定是后提交的用户将覆盖掉前者提交的结果了。
这个直接可以使用加锁机制去解决,乐观锁或者悲观锁。
乐观锁,就是在数据库设计一个版本号的字段,每次修改都使其+1,这样在提交时比对提交前的版本号就知道是不是并发提交了,但是有个缺点就是只能是应用中控制,如果有跨应用修改同一条数据乐观锁就没办法了,这个时候可以考虑悲观锁。
悲观锁,就是直接在数据库层面将数据锁死,类似于oralce中使用select xxxxx from xxxx where xx=xx for update,这样其他线程将无法提交数据。
除了加锁的方式也可以使用接收锁定的方式,思路是在数据库中设计一个状态标识位,用户在对数据进行修改前,将状态标识位标识为正在编辑的状态,这样其他用户要编辑此条记录时系统将发现有其他用户正在编辑,则拒绝其编辑的请求,类似于你在操作系统中某文件正在执行,然后你要修改该文件时,系统会提醒你该文件不可编辑或删除。4、不建议在数据库层面加锁,建议通过服务端的内存锁(锁主键)。当某个用户要修改某个id的数据时,把要修改的id存入memcache,若其他用户触发修改此id的数据时,读到memcache有这个id的值时,就阻止那个用户修改。
5、实际应用中,并不是让mysql去直面大并发读写,会借助“外力”,比如缓存、利用主从库实现读写分离、分表、使用队列写入等方法来降低并发读写。
-
数据库并发控制的锁机制
2017-09-13 09:46:15
1 前言
数据库大并发操作要考虑死锁和锁的性能问题。看到网上大多语焉不详(尤其更新锁),所以这里做个简明解释,为下面描述方便,这里用T1代表一个数据库执行请求,T2代表另一个请求,也可以理解为T1为一个线程,T2 为另一个线程。T3,T4以此类推。下面以SQL Server(2005)为例。
2 锁的种类
- 共享锁(Shared lock)。
例1: ---------------------------------------- T1: select * from table (请想象它需要执行1个小时之久,后面的sql语句请都这么想象) T2: update table set column1='hello' 过程: T1运行 (加共享锁) T2运行 If T1 还没执行完 T2等...... else 锁被释放 T2执行 endif T2之所以要等,是因为T2在执行update前,试图对table表加一个排他锁, 而数据库规定同一资源上不能同时共存共享锁和排他锁。所以T2必须等T1 执行完,释放了共享锁,才能加上排他锁,然后才能开始执行update语句。 例2: ---------------------------------------- T1: select * from table T2: select * from table 这里T2不用等待T1执行完,而是可以马上执行。 分析: T1运行,则table被加锁,比如叫lockA T2运行,再对table加一个共享锁,比如叫lockB。 两个锁是可以同时存在于同一资源上的(比如同一个表上)。这被称为共 享锁与共享锁兼容。这意味着共享锁不阻止其它session同时读资源,但阻 止其它session update 例3: ---------------------------------------- T1: select * from table T2: select * from table T3: update table set column1='hello' 这次,T2不用等T1运行完就能运行,T3却要等T1和T2都运行完才能运行。 因为T3必须等T1和T2的共享锁全部释放才能进行加排他锁然后执行update 操作。 例4:(死锁的发生) ---------------------------------------- T1: begin tran select * from table (holdlock) (holdlock意思是加共享锁,直到事物结束才释放) update table set column1='hello' T2: begin tran select * from table(holdlock) update table set column1='world' 假设T1和T2同时达到select,T1对table加共享锁,T2也对加共享锁,当 T1的select执行完,准备执行update时,根据锁机制,T1的共享锁需要升 级到排他锁才能执行接下来的update.在升级排他锁前,必须等table上的 其它共享锁释放,但因为holdlock这样的共享锁只有等事务结束后才释放, 所以因为T2的共享锁不释放而导致T1等(等T2释放共享锁,自己好升级成排 他锁),同理,也因为T1的共享锁不释放而导致T2等。死锁产生了。 例5: ---------------------------------------- T1: begin tran update table set column1='hello' where id=10 T2: begin tran update table set column1='world' where id=20 这种语句虽然最为常见,很多人觉得它有机会产生死锁,但实际上要看情 况,如果id是主键上面有索引,那么T1会一下子找到该条记录(id=10的记 录),然后对该条记录加排他锁,T2,同样,一下子通过索引定位到记录, 然后对id=20的记录加排他锁,这样T1和T2各更新各的,互不影响。T2也不 需要等。 但如果id是普通的一列,没有索引。那么当T1对id=10这一行加排他锁后, T2为了找到id=20,需要对全表扫描,那么就会预先对表加上共享锁或更新 锁或排他锁(依赖于数据库执行策略和方式,比如第一次执行和第二次执行 数据库执行策略就会不同)。但因为T1已经为一条记录加了排他锁,导致 T2的全表扫描进行不下去,就导致T2等待。 死锁怎么解决呢?一种办法是,如下: 例6: ---------------------------------------- T1: begin tran select * from table(xlock) (xlock意思是直接对表加排他锁) update table set column1='hello' T2: begin tran select * from table(xlock) update table set column1='world' 这样,当T1的select 执行时,直接对表加上了排他锁,T2在执行select时,就需要等T1事物完全执行完才能执行。排除了死锁发生。 但当第三个user过来想执行一个查询语句时,也因为排他锁的存在而不得不等待,第四个、第五个user也会因此而等待。在大并发 情况下,让大家等待显得性能就太友好了,所以,这里引入了更新锁。
- 更新锁(Update lock)
为解决死锁,引入更新锁。 例7: ---------------------------------------- T1: begin tran select * from table(updlock) (加更新锁) update table set column1='hello' T2: begin tran select * from table(updlock) update table set column1='world' 更新锁的意思是:“我现在只想读,你们别人也可以读,但我将来可能会做更新操作,我已经获取了从共享锁(用来读)到排他锁 (用来更新)的资格”。一个事物只能有一个更新锁获此资格。 T1执行select,加更新锁。 T2运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。 当后来有user3、user4...需要查询table表中的数据时,并不会因为T1的select在执行就被阻塞,照样能查询,相比起例6,这提高 了效率。 例8: ---------------------------------------- T1: select * from table(updlock) (加更新锁) T2: select * from table(updlock) (等待,直到T1释放更新锁,因为同一时间不能在同一资源上有两个更新锁) T3: select * from table (加共享锁,但不用等updlock释放,就可以读) 这个例子是说明:共享锁和更新锁可以同时在同一个资源上。这被称为共享锁和更新锁是兼容的。 例9: ---------------------------------------- T1: begin select * from table(updlock) (加更新锁) update table set column1='hello' (重点:这里T1做update时,不需要等T2释放什么,而是直接把更新锁升级为排他锁,然后执行update) T2: begin select * from table (T1加的更新锁不影响T2读取) update table set column1='world' (T2的update需要等T1的update做完才能执行) 我们以这个例子来加深更新锁的理解, 第一种情况:T1先达,T2紧接到达;在这种情况中,T1先对表加更新锁,T2对表加共享锁,假设T2的select先执行完,准备执行update, 发现已有更新锁存在,T2等。T1执行这时才执行完select,准备执行update,更新锁升级为排他锁,然后执行update,执行完成,事务 结束,释放锁,T2才轮到执行update。 第二种情况:T2先达,T1紧接达;在这种情况,T2先对表加共享锁,T1达后,T1对表加更新锁,假设T2 select先结束,准备 update,发现已有更新锁,则等待,后面步骤就跟第一种情况一样了。 这个例子是说明:排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。
- 排他锁(独占锁,Exclusive Locks)
这个简单,即其它事务既不能读,又不能改排他锁锁定的资源。 例10 T1: update table set column1='hello' where id<1000 T2: update table set column1='world' where id>1000 假设T1先达,T2随后至,这个过程中T1会对id<1000的记录施加排他锁.但不会阻塞T2的update。 例11 (假设id都是自增长且连续的) T1: update table set column1='hello' where id<1000 T2: update table set column1='world' where id>900 如同例10,T1先达,T2立刻也到,T1加的排他锁会阻塞T2的update.
- 意向锁(Intent Locks)
意向锁就是说在屋(比如代表一个表)门口设置一个标识,说明屋子里有人(比如代表某些记录)被锁住了。另一个人想知道屋子 里是否有人被锁,不用进屋子里一个一个的去查,直接看门口标识就行了。 当一个表中的某一行被加上排他锁后,该表就不能再被加表锁。数据库程序如何知道该表不能被加表锁?一种方式是逐条的判断该 表的每一条记录是否已经有排他锁,另一种方式是直接在表这一层级检查表本身是否有意向锁,不需要逐条判断。显然后者效率高。 例12: ---------------------------------------- T1: begin tran select * from table (xlock) where id=10 --意思是对id=10这一行强加排他锁 T2: begin tran select * from table (tablock) --意思是要加表级锁 假设T1先执行,T2后执行,T2执行时,欲加表锁,为判断是否可以加表锁,数据库系统要逐条判断table表每行记录是否已有排他锁, 如果发现其中一行已经有排他锁了,就不允许再加表锁了。只是这样逐条判断效率太低了。 实际上,数据库系统不是这样工作的。当T1的select执行时,系统对表table的id=10的这一行加了排他锁,还同时悄悄的对整个表 加了意向排他锁(IX),当T2执行表锁时,只需要看到这个表已经有意向排他锁存在,就直接等待,而不需要逐条检查资源了。 例13: ---------------------------------------- T1: begin tran update table set column1='hello' where id=1 T2: begin tran update table set column1='world' where id=1 这个例子和上面的例子实际效果相同,T1执行,系统对table同时对行家排他锁、对页加意向排他锁、对表加意向排他锁。
- 计划锁(Schema Locks)
例14: ---------------------------------------- alter table .... (加schema locks,称之为Schema modification (Sch-M) locks DDL语句都会加Sch-M锁 该锁不允许任何其它session连接该表。连都连不了这个表了,当然更不用说想对该表执行什么sql语句了。 例15: ---------------------------------------- 用jdbc向数据库发送了一条新的sql语句,数据库要先对之进行编译,在编译期间,也会加锁,称之为:Schema stability (Sch-S) locks select * from tableA 编译这条语句过程中,其它session可以对表tableA做任何操作(update,delete,加排他锁等等),但不能做DDL(比如alter table)操作。
- Bulk Update Locks 主要在批量导数据时用(比如用类似于oracle中的imp/exp的bcp命令)。不难理解,程序员往往也不需要关心,不赘述了。
3 何时加锁?
如何加锁,何时加锁,加什么锁,你可以通过hint手工强行指定,但大多是数据库系统自动决定的。这就是为什么我们可以不懂锁也可 以高高兴兴的写SQL。 例15: ---------------------------------------- T1: begin tran update table set column1='hello' where id=1 T2: SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED -- 事物隔离级别为允许脏读 go select * from table where id=1 这里,T2的select可以查出结果。如果事物隔离级别不设为脏读,则T2会等T1事物执行完才能读出结果。 数据库如何自动加锁的? 1) T1执行,数据库自动加排他锁 2) T2执行,数据库发现事物隔离级别允许脏读,便不加共享锁。不加共享锁,则不会与已有的排他锁冲突,所以可以脏读。 例16: ---------------------------------------- T1: begin tran update table set column1='hello' where id=1 T2: select * from table where id=1 --为指定隔离级别,则使用系统默认隔离级别,它不允许脏读 如果事物级别不设为脏读,则: 1) T1执行,数据库自动加排他锁 2) T2执行,数据库发现事物隔离级别不允许脏读,便准备为此次select过程加共享锁,但发现加不上,因为已经有排他锁了,所以就 等啊等。直到T1执行完,释放了排他锁,T2才加上了共享锁,然后开始读....
4 锁的粒度
锁的粒度就是指锁的生效范围,就是说是行锁,还是页锁,还是整表锁. 锁的粒度同样既可以由数据库自动管理,也可以通过手工指定hint来管理。
例17: ---------------------------------------- T1: select * from table (paglock) T2: update table set column1='hello' where id>10 T1执行时,会先对第一页加锁,读完第一页后,释放锁,再对第二页加锁,依此类推。假设前10行记录恰好是一页(当然,一般不可能 一页只有10行记录),那么T1执行到第一页查询时,并不会阻塞T2的更新。 例18: ---------------------------------------- T1: select * from table (rowlock) T2: update table set column1='hello' where id=10 T1执行时,对每行加共享锁,读取,然后释放,再对下一行加锁;T2执行时,会对id=10的那一行试图加锁,只要该行没有被T1加上行锁, T2就可以顺利执行update操作。 例19: ---------------------------------------- T1: select * from table (tablock) T2: update table set column1='hello' where id = 10 T1执行,对整个表加共享锁. T1必须完全查询完,T2才可以允许加锁,并开始更新。 以上3例是手工指定锁的粒度,也可以通过设定事物隔离级别,让数据库自动设置锁的粒度。不同的事物隔离级别,数据库会有不同的 加锁策略(比如加什么类型的锁,加什么粒度的锁)。具体请查联机手册。
5 锁与事物隔离级别的优先级
手工指定的锁优先, 例20: ---------------------------------------- T1: GO SET TRANSACTION ISOLATION LEVEL SERIALIZABLE GO BEGIN TRANSACTION SELECT * FROM table (NOLOCK) GO T2: update table set column1='hello' where id=10 T1是事物隔离级别为最高级,串行锁,数据库系统本应对后面的select语句自动加表级锁,但因为手工指定了NOLOCK,所以该select 语句不会加任何锁,所以T2也就不会有任何阻塞。
6 数据库的其它重要Hint以及它们的区别
1) holdlock 对表加共享锁,且事物不完成,共享锁不释放。 2) tablock 对表加共享锁,只要statement不完成,共享锁不释放。 与holdlock区别,见下例: 例21 ---------------------------------------- T1: begin tran select * from table (tablock) T2: begin tran update table set column1='hello' where id = 10 T1执行完select,就会释放共享锁,然后T2就可以执行update. 此之谓tablock. 下面我们看holdlock 例22 ---------------------------------------- T1: begin tran select * from table (holdlock) T2: begin tran update table set column1='hello' where id = 10 T1执行完select,共享锁仍然不会释放,仍然会被hold(持有),T2也因此必须等待而不能update. 当T1最后执行了commit或 rollback说明这一个事物结束了,T2才取得执行权。 3) TABLOCKX 对表加排他锁 例23: ---------------------------------------- T1: select * from table(tablockx) (强行加排他锁) 其它session就无法对这个表进行读和更新了,除非T1执行完了,就会自动释放排他锁。 例24: ---------------------------------------- T1: begin tran select * from table(tablockx) 这次,单单select执行完还不行,必须整个事物完成(执行了commit或rollback后)才会释放排他锁。 4) xlock 加排他锁 那它跟tablockx有何区别呢? 它可以这样用, 例25: ---------------------------------------- select * from table(xlock paglock) 对page加排他锁 而TABLELOCX不能这么用。 xlock还可这么用:select * from table(xlock tablock) 效果等同于select * from table(tablockx)
7 锁的超时等待
例26
SET LOCK_TIMEOUT 4000 用来设置锁等待时间,单位是毫秒,4000意味着等待 4秒可以用select @@LOCK_TIMEOUT查看当前session的锁超时设置。-1 意味着 永远等待。 T1: begin tran udpate table set column1='hello' where id = 10 T2: set lock_timeout 4000 select * from table wehre id = 10
T2执行时,会等待T1释放排他锁,等了4秒钟,如果T1还没有释放排他锁,T2就会抛出异常: Lock request time out period exceeded.
8 附:各种锁的兼容关系表
| Requested mode | IS | S | U | IX | SIX | X | | Intent shared (IS) | Yes | Yes | Yes | Yes | Yes | No | | Shared (S) | Yes | Yes | Yes | No | No | No | | Update (U) | Yes | Yes | No | No | No | No | | Intent exclusive (IX) | Yes | No | No | Yes | No | No | | Shared with intent exclusive (SIX) | Yes | No | No | No | No | No | | Exclusive (X) | No | No | No | No | No | No |
- 共享锁(Shared lock)。
我的栏目
标题搜索
我的存档
数据统计
- 访问量: 334310
- 日志数: 230
- 建立时间: 2007-11-19
- 更新时间: 2024-04-03