为什么需要锁
我们在之前介绍的都是关于只有一个用户操作数据库的情况,但是在实际的项目中,面临的更多情况是多用户操作数据库。例如电商平台的秒杀系统等,可能会在短时间具有多个用户对数据库进行操作,如果没有进行特殊的处理,这是极其容易造成数据冲突的。我们下面举例说明一下发生冲突的一种情况
如图,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。所以为了避免这样的情况发生,就必须使用锁定。锁定是为了当某个用户在进行操作而拒绝其他用户操作的一种机制,解除锁定时被称为解锁。 加锁以后的流程大致为:
锁的种类
按照使用的目的可以分为:
共享锁(Shared Lock,也叫S锁): 共享锁是当用户参照数据时,将数据对象变为只读形式的锁定。例如在上面的流程中,A用户第一次获取账户余额信息的行为并不会被拒绝,但是,A用户在C用户结束操作之前,依然是无法对数据进行修改的。也被称为:读取锁定
排他锁 (Exclusive Lock,也叫X锁): 排他锁是使用【INSERT】,【UPDATE】,【DELETE】命令对数据进行修改时,使用的机制。例如在上面的流程中,使用的就是排他锁,即A用户的第一次读取操作也会被拒绝。只有当C用户完成所有操作以后,A用户才能进行操作。也被称为写入锁定或者独占锁定
注意事项
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
锁的粒度
表锁(数据表): 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,一般是做ddl处理时使用。
行锁(记录): 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高,MySQL一般都是用行锁来处理并发事务
页锁(数据库): 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
锁定的粒度会影响程序的并发数。一般情况下,锁定的粒度越小,并发性才会更高。例如:在使用了行锁的情况下,还可以对同一数据表的不同行进行数据的处理,而如果使用了表锁定,其他进程只能等到前一个进程完成了事务处理后才能进行操作。那么是不是锁定的粒度越小越好呢?其实不然,因为锁定会极大的消耗着数据的资源,也就是说,锁定的数目越多,消耗的服务的资源也就越多。
注意事项
如果数据库中行单位粒度的锁定大量发生的情况时,数据库有将这些锁定的粒度自动向上提升的机制,被称为锁定提升(Lock Escalation)
MySQL引擎对应支持的锁
根据不同的存储引擎,MySQL中锁的特性可以大致归纳如下:
注意事项
目前主要有两种锁定协议:
一段锁协议:为了预防在高并发的环境中发生死锁的情况,事先就将需要使用到数据全部进行锁定,等所有操作结束后再进行解锁。
两段锁协议: 将事务分成两个阶段,加锁阶段和解锁阶段。
加锁阶段:在对任何数据进行读操作之前要申请并获得共享锁(其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得排它锁(其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
InnoDB采用的是两阶段锁定协议,因为在事务开始阶段,数据库并不知道会用到哪些数据。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据事务隔离级别在需要的时候自动加锁。
InnoDB引擎中锁的使用
表锁
使用情形
需要更新或插入大量数据且表结构又比较复杂。在这种情况是使用行锁很容易造成锁冲突和长时间的等待。
事务涉及多表的操作,如果在这种情况使用行锁,很容易引起死锁,造成大量的事务回滚。
注意事项
如果多次涉及到上述两种事务情形,可根据实际情况考虑是否使用MyISAM引擎。
在用【 LOCK TABLES】对【InnoDB】表加锁时要注意,要将【AUTOCOMMIT】设为0,否则MySQL不会给表加锁;事务结束前,不要用【UNLOCK TABLES】释放表锁,因为【UNLOCK TABLES】会隐含地提交事务;【COMMIT】或【ROLLBACK】并不能释放用【LOCK TABLES】加的表级锁,必须用【UNLOCK TABLES】释放表锁。
使用【LOCK TABLES】虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当【autocommit】为0、【InnoDBtablelocks】=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁,否则,InnoDB将无法自动检测并处理这种死锁。
行锁
注意事项
在InnoDB引擎中行锁是通过给索引上的索引项加锁来实现的,也就意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
乐观锁,悲观锁,死锁
悲观锁(Pessimistic Lock):假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。简要说就是:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会进入等待状态。例如:Java中的【synchronized】 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
乐观锁(Optimistic Lock):假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性
它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持开放态度。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题,乐观锁适用于读多写少的应用场景,这样大大提高吞吐量。乐观锁主要的实现方式有:
使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
死锁(Deadlock):两个或两个以上的进程在执行过程中,因争夺资源而造成的互相等待
在多任务系统下,当一个或多个进程等待系统资源,而资源又被进程本身或其他进程占用时,就形成了死锁。在数据库中,因为MyISAM总是一次性获得所需的全部锁,因此不会出现死锁。所以死锁主要发生于InnoDB引擎中。但是,发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数【innodblockwait_timeout】 来解决。这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序为访问表,这样可以大大降低产生死锁的机会。如果两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可能避免。
在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低死锁的可能。 例如:
在开发中,使用【insert into test(xx,xx) on duplicate key update ='XX';】来解决:根据字段值查询(有索引),如果不存在,则插入;否则更新的需求。否则很容易出现死锁,例如:
到这里,我们就讲解完了关于锁与事务处理分离水平的第一部分的内容,后面我们将介绍关于事务处理分离水平。
参考文献
美团点评技术团队(ameng ·2014-08-20 15:50).Innodb中的事务隔离级别和锁的关系 博文地址:https://tech.meituan.com/innodb_lock.html
《MySQL性能优化与架构设计》
领取专属 10元无门槛券
私享最新 技术干货