我们在之前介绍的都是关于只有一个用户操作数据库的情况,但是在实际的项目中,面临的更多情况是多用户操作数据库。例如电商平台的秒杀系统等,可能会在短时间具有多个用户对数据库进行操作,如果没有进行特殊的处理,这是极其容易造成数据冲突的。我们下面举例说明一下发生冲突的一种情况
如图,A用户和C用户共有一个余额为1000元的公用账户,这天,A和C同时到了不同银行,A和C同时获得了账户的余额信息,这时间A就决定往账户中存入1000元,C决定从账户中取出1000元。正常情况下:账户余额依旧为1000元。但是如果不采取措施,那么数据库只会保存A用户的2000元的余额信息。这是为啥呢?当A和C拿到余额信息时,账户余额为1000元。假设C用户取款需要1min,A用户存款需要2min.那么当C用户取款成功后,账户余额被更改为0。但是此时的用户A并不知道。她仍然是按照账户还有1000元的基础上进行操作,那么当他反馈给账户的时候,1min前余额才被更改为0的账户就会被更改为2000。所以为了避免这样的情况发生,就必须使用锁定。锁定是为了当某个用户在进行操作而拒绝其他用户操作的一种机制,解除锁定时被称为解锁。
加锁以后的流程大致为:
按照使用的目的可以分为:
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
锁定的粒度会影响程序的并发数。一般情况下,锁定的粒度越小,并发性才会更高。例如:在使用了行锁的情况下,还可以对同一数据表的不同行进行数据的处理,而如果使用了表锁定,其他进程只能等到前一个进程完成了事务处理后才能进行操作。那么是不是锁定的粒度越小越好呢?其实不然,因为锁定会极大的消耗着数据的资源,也就是说,锁定的数目越多,消耗的服务的资源也就越多。
根据不同的存储引擎,MySQL中锁的特性可以大致归纳如下:
存储引擎 | 行锁 | 表锁 | 页锁 |
---|---|---|---|
MyISAM | √ | ||
BDB | √ | √ | |
InnoDB | √ | √ |
目前主要有两种锁定协议:
InnoDB采用的是两阶段锁定协议,因为在事务开始阶段,数据库并不知道会用到哪些数据。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据事务隔离级别在需要的时候自动加锁。
它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。简要说就是:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会进入等待状态。例如:Java中的【synchronized】 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持开放态度。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题,乐观锁适用于读多写少的应用场景,这样大大提高吞吐量。乐观锁主要的实现方式有:
在多任务系统下,当一个或多个进程等待系统资源,而资源又被进程本身或其他进程占用时,就形成了死锁。在数据库中,因为MyISAM总是一次性获得所需的全部锁,因此不会出现死锁。所以死锁主要发生于InnoDB引擎中。但是,发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数【innodb_lock_wait_timeout】 来解决。这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序为访问表,这样可以大大降低产生死锁的机会。如果两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可能避免。
在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低死锁的可能。 例如:
Session1:
mysql> select * from test where id in (8,9) for update;
+----+--------+------+
| id | course | name |
+----+--------+------+
| 8 | XXX | xxx |
| 9 | FFF | fff |
+----+--------+------+
2 rows in set (0.04 sec)
Session2:
select * from test where id in (10,8,5) for update;
//锁等待中……
//其实这个时候id=10这条记录没有被锁住的,但id=5的记录已经被锁住了,锁的等待在id=8的这里。
Session3:
mysql> select * from test where id=5 for update;
//锁等待中
Session4:
mysql> select * from test where id=10 for update;
+----+--------+------+
| id | course | name |
+----+--------+------+
| 10 | KKK | kkk |
+----+--------+------+
1 row in set (0.00 sec)
在其它session中id=5是加不了锁的,但是id=10是可以加上锁的。
xx
=’XX’;】来解决:根据字段值查询(有索引),如果不存在,则插入;否则更新的需求。否则很容易出现死锁,例如:以id为主键为例,目前还没有id=22的行
Session1:
select * from t3 where id=22 for update;
Empty set (0.00 sec)
session2:
select * from t3 where id=23 for update;
Empty set (0.00 sec)
Session1:
insert into t3 values(22,'ac','a',now());
锁等待中……
Session2:
insert into t3 values(23,'bc','b',now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
到这里,我们就讲解完了关于锁与事务处理分离水平的第一部分的内容,后面我们将介绍关于事务处理分离水平。
美团点评技术团队(ameng ·2014-08-20 15:50).Innodb中的事务隔离级别和锁的关系 博文地址:https://tech.meituan.com/innodb_lock.html
《MySQL性能优化与架构设计》