mysql事务和锁 如何应对秒杀抢购高并发思路

参考: 

1,如何应对秒杀抢购高并发思路

2, MySQL InnoDB中,乐观锁、悲观锁、共享锁、排它锁、行锁、表锁、死锁概念的理解

一,锁

锁是并发控制中最核心的概念之一,在MySQL中的锁分两大类,一种是读锁,一种是写锁,读锁也能够称为共享锁(shared lock),写锁也一般称为排它锁(exclusive lock)。php

  这里先不讨论锁的具体实现,描述一下锁的概念:读锁是共享的,或者说是相互不阻塞的。多个客户在同一时刻能够同时读取一个资源,且互不干扰。写锁则是排他的,就是说一个写锁会阻塞其余的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定时间里,只有一个用户能执行写入,并防止其余用户读取正在写入的同一资源。另外在通常状况下,写锁比读锁优先级高。html

  MySQL中的锁有两种粒度,一种是表锁,在表级别加锁,是MySQL中最基本的锁策略,而且开销最小,这种锁的并发性能较低;另外一种为行锁,在行级加锁,并发性较高。表锁与行锁没有绝对的性能强弱之分,在应用中能够根据实际场景选择,在锁粒度与数据安全之间寻求一种平衡机制。mysql

  InnoDB的行锁是基于索引实现的,若是不经过索引访问数据,InnoDB会使用表锁。sql

  锁的具体实现协议大致分为两种:显式锁和隐式锁。显式锁是指根据用户须要手动去请求的锁。隐式锁则是指存储引擎自行根据须要施加的锁。显式锁的用法示例:数据库

  例1:开启两个ssh链接同一主机,进入MySQL,在链接A上对表tbl2作读锁操做:缓存

1 mysql> USE mysql;
2 mysql> LOCK TABLE tbl2 READ;

  在链接B上读取数据是能够的,可是写入数据不行:安全

1 mysql> USE mysql;
2 mysql> SELECT * FROM tbl2;
3 mysql> INSERT INTO tbl2 VALUES (1,'tom'); #会一直卡在这一步,不向后执行。

  当在链接1上将tbl2解锁后,就能写入数据了:session

 

 

二,事务

在php与数据库的交互中,若是并发量大,而且都去进行数据库的修改的话,就有一个问题须要注意.数据的锁问题.就会牵扯数据库的事务跟隔离机制

数据库事务依照不一样的事务隔离级别来保证事务的ACID特性,也就是说事务不是一开启就能解决全部并发问题。一般状况下,这里的并发操做可能带来四种问题:并发

  • 更新丢失:一个事务的更新覆盖了另外一个事务的更新,这里出现的就是丢失更新的问题。
  • 脏读:一个事务读取了另外一个事务未提交的数据。
  • 不可重复读:一个事务两次读取同一个数据,两次读取的数据不一致。
  • 幻象读:一个事务两次读取一个范围的记录,两次读取的记录数不一致。

三,事务隔离级别

  事务的隔离级别有四种:oracle

1.READ UNCOMMITTED(未提交读)

  在READ UNCOMMITTED级别,事务中的修改即便没有提交,对其它事务也都是可见的。即事务可读取未提交的数据,这称为脏读(Dirty Read)。这会致使不少问题,在实际应用中通常不多用到。

2.READ COMMITED(提交读)

  READ COMMITTED表示只能读取事务修改提交后的数据。此级别有时候也叫作不可重复读(nonrepeatable read),由于两次执行一样的查询,可能会获得不同的结果。

3.REPEATABLE READ(可重复读)

  此级别解决了脏读的问题,保证了在同一个事务中屡次一样记录的结果是一致的。但会带来新的问题——幻读(Phantom Read)。MySQL默认使用此级别。

4.SERIALIZABLE(可串行化)

  SERIALIZABLE是最高的隔离级别。它会强制事务串行执行,避免了幻读、脏读的问题,可是牺牲了并发性。

  示例:

mysql> SELECT @@session.tx_isolation; #查看当前事务隔离级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)

mysql> SET @@session.tx_isolation='级别'; 可设置隔离级别

 

四,乐观锁和悲观锁

乐观锁和悲观锁是一种机制不是指具体的锁。

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。假定会发生并发冲突,屏蔽一切可能违反数据完整性的操做。

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,乐观锁适用于读多写少的应用场景,这样能够提升并发粒度。

悲观锁实现:事务隔离级别的可串行化就是典型的悲观锁机制,读加读锁,写加写锁。这是基于锁的并发控制。

乐观锁实现:innoDB引擎的乐观锁机制是经过MVCC实现的。叫多版本并发控制(读不加锁读写不冲突),是经过保存数据在某一个时间点的快照来实现的。所以每个事务不管执行多长时间看到的数据,都是同样的。

增删改操做都属于当前读,不会读取历史版本的快照数据。

普通的select属于快照读,读取的是快照数据也多是历史数据。

手动加锁的select是当前读 例如:select * from tb for update或者select * from tb lock in share mode

涉及抢购、秒杀、抽奖、抢票等活动时,为了不超卖,那么库存数量是有限的,可是若是同时下单人数超过了库存数量,就会致使商品超卖问题。那么咱们怎么来解决这个问题呢,个人思路以下(伪代码): 

sql1:查询商品库存
if(库存数量 > 0)
{
  //生成订单...
  sql2:同时库存-1
}

当没有并发时,上面的流程看起来是再正常不过了,假设同时两我的下单,而库存只有1个了,在sql1阶段两我的查询到的库存都是>0的,因而最终都执行了sql2,库存最后变为-1,超售了,这不是咱们想要的结果吧。

解决这个问题比较流行的思路我总结了下:
1.用额外的单进程处理一个队列,下单请求放到队列里,一个个处理,就不会有并发的问题了,可是要额外的开启后台进程以及延迟问题,这里暂不予考虑。这里我可以使用消息队列,咱们经常使用到Memcacheq、Radis。 好比:有100张票可供用户抢,那么就能够把这100张票放到缓存中,读写时不要加锁。 当并发量大的时候,可能有500人左右抢票成功,这样对于500后面的请求能够直接转到活动结束的静态页面。进去的500我的中有400我的是不可能得到商品的。因此能够根据进入队列的前后顺序只能前100我的购买成功。后面400我的就直接转到活动结束页面。固然进去500我的只是举个例子,至于多少能够本身调整。而活动结束页面必定要用静态页面,不要用数据库。这样就减轻了数据库的压力。
2.mysql乐观锁,意思是好比总库存是2,抢购事件提交时,立马将库存+1,那么此时库存是3,而后订单生成后,在更新库存前再查询一次库存(由于订单生成理所固然库存-1,可是先不急,再查一次库存返回结果是3),看看跟预期的库存数量(这里预期的库存是3)是否保持一致,不一致就回滚,提示用户库存不足。这里说道悲观锁,可能有朋友会问,那必定有乐观锁了吧??这里我就浅谈下我所了解的悲观与乐观锁了

 

1,悲观锁(Pessimistic Lock)

悲观锁的特色是先获取锁,再进行业务操做,即“悲观”的认为获取锁是很是有可能失败的,所以要先确保获取锁成功再进行业务操做。一般所说的“一锁二查三更新”即指的是使用悲观锁。

一般来说在数据库上的悲观锁须要数据库自己提供支持,即经过经常使用的select … for update操做来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,所以其余并发执行的select for update若是试图选中同一行则会发生排斥(须要等待行锁被释放),所以达到锁的效果。

select for update获取的行锁会在当前事务结束时自动释放,所以必须在事务中使用。

这里须要注意的一点是不一样的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示若是拿不到锁马上报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中全部扫描过的行都会被锁上,这一点很容易形成问题。

所以若是在mysql中用悲观锁务必要肯定走了索引,而不是全表扫描。

2,乐观锁(Optimistic Lock)

在每次去拿数据的时候认为别人不会修改,不对数据上锁,可是在提交更新的时候会判断在此期间数据是否被更改,若是被更改则提交失败。

乐观锁实现:使用版本控制字段,再利用行锁的特性实现乐观锁

有一张订单表order,有字段id、order_no、 price,  为实现乐观锁控制,添加version字段,默认值为0

id 1
order_no 123456
price 5
version 0

假设两我的同时进来修改该条数据,操做为:

1. 先查询该数据   select * from order where id = 1

2. 修改该条数据  update order set price = 1 where id = 1 and price = 5

若是两我的同时查询到该条数据price = 5, 能够执行update操做, 但任意一方还没执行update操做,那么最后双方都执行update,致使数据被修改两次,这样很容易产生错误数据(脏数据)

使用version字段控制版本后:

1. 两人先查询该数据 select * from order where id = 1

此时两人查询到的数据同样,id = 1, price = 5, order_no = 123456, version = 0

2. 两人都发现该条数据price = 5, 符合update条件,第一人执行update(由于mysql行锁的特性,两人不可能同时修改一条数据,因此update同一条数据的时候,是有前后顺序的,只有在第一个执行完update,才能释放行锁,第二个继续进行update): 

update order set price = 1, version = version + 1 where id = 1 and price = 5 and version = 0

执行完成后,version字段值将变成1, 第二人执行update:

update order set price = 1, version = version + 1 where id = 1 and price = 5 and version = 0

此时的version的值已经被修改成1,因此第二人修改失败,实现乐观锁控制。

相关文章
相关标签/搜索