MySQL锁机制
一、表锁、页锁、行锁
为了保证数据的一致性,数据库提供了不同的锁机制,让各线程有序地访问数据库资源。
MySQL数据库存在多种数据存储引擎,每种存储引擎所针对的应用场景都不一样。为了满足各自特定应用场景的需求,各存储引擎的锁机制也有较大区别。
MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。
1、表锁
概念:表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。大部分mysql引擎都支持表锁,MyISAM和InnoDB都支持表级锁。MyISAM默认使用表级锁。
特点:
1)偏向MyISM存储引擎,开销小,加锁快;
2)不会产生死锁;
3)锁的粒度大,发生锁冲突的概率最高,并发度最低。
2、行锁
概念:行级锁是Mysql中锁粒度最小的一种锁,表示只对当前操作的行进行加锁,而不是对整张表加锁。InnoDB存储引擎默认使用行级锁。
特点:
1)加锁粒度最小,但加锁的开销最大;
2)大大减少数据库操作的冲突,但有可能会出现死锁的情况。
注意:InnoDB行锁是通过给索引的索引项加锁来实现的,只有通过索引条件操作数据时,InnoDB才使用行锁。否则,InnoDB将使用表锁。在实际应用中,需要注意InnoDB行锁的特性。否则,容易导致大量锁冲突,从而影响并发性能。
1)在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。
测试方式:A. 在一个mysql终端界面 set autocommit = 0,开启手动提交事务;然后begin、commit控制事务;
B. 在另一个mysql终端界面输入insert、delete、update语句进行测试。
2)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行。另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
3)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划来决定的。如果MySQL认为全表扫描效率更高,比如一些很小的表,它就不会使用索引。这种情况下,InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,需要检查SQL语句的执行计划,以确认是否真正使用了索引。
3、页锁
概念:页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多。行级锁冲突少,但速度慢。所以处于折衷的页级锁,一次锁定相邻的一组记录。BDB引擎支持页级锁。
特点:处于表锁和行锁之间。
MyISAM和MEMORY存储引擎采用的是表级锁(table-level-locking);BDB存储引擎采用的是页面锁(page-level-locking),同时也支持表级锁;InnoDB存储引擎既支持行级锁,也支持表级锁,默认情况下是采用行级锁。
二、乐观锁、悲观锁
乐观锁和悲观锁不是指具体的两种锁,而是指两种常见资源并发锁的设计思路,也是并发编程中一个非常基础的概念。
1、乐观锁
概念:就是对一切操作持乐观态度。它假设不会发生并发冲突,只在提交时检查是否违反了数据完整性。
特点:
1)提交时才去锁定,所以不会产生任何锁和死锁问题;
2)只能防止脏读后数据的提交,不能解决脏读问题;
3)适合读多写/更新少的场景。因为过多的写/更新操作将导致version比较不一致的概率大大增加。
4)并发高时需要按实际情况处理。
实现方式:
1)表中增加version字段,查询表中数据时,获取version版本号,然后版本号+1。修改表中数据时,判断version版本号是否大于存储的版本号,如果是则操作成功,如果不是则说明已经有其他线程更新了数据,操作失败。
update table set status = 1,version = version+1 where id = #{id} and version = #{version};
2)实现方式和1)差不多,是在表增加一个时间戳(timestamp)字段。和上面的version字段类似。在更新提交时,检查当前数据库中数据的时间戳,用来和更新前取到的时间戳进行对比,如果一致就更新,否则就失败。
3)Java中无锁,CompareAndSwap即CAS,也是采用了乐观锁设计思路。CAS操作包含三个参数CAS(V,E,N):V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才将V的值设为N。如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
2、悲观锁
概念:就是对一切操作持悲观态度。它假设一定会有并发冲突,先获取锁,再进行业务操作。操作过程中强行独占资源,在整个数据处理过程中,将数据处于锁定状态。
特点:
1)适合写/更新多读少的应用场景。写/更新操作多的情况,容易导致数据不一致,所以使用悲观锁可以保证有序操作数据。
2)并发高时需要按实际情况处理。并发高时,悲观锁可以保证数据的一致性,但会导致线程串行执行,会影响系统性能。
实现方式:
1)关系型数据库里的表锁、行锁,读锁、写锁等都属于悲观锁。
2)MySQL中使用select … for update操作来实现悲观锁。但需要注意,select ... for update语句执行过程中,所有被扫描过的行都会加上锁。因此,使用select ... for update必须确定使用了索引,否则将是锁表。
3)Java里面线程同步相关的synchronized、lock也属于悲观锁。
总结:乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此乐观锁适合用在取锁失败概率比较小的场景,可以提升系统并发性能。
三、读锁(共享锁)、写锁(排它锁)
1、读锁
概念:读锁又叫共享锁或S锁。若事务T对数据对象A加上了S锁,则事务T可以读A但不能修改A。其他事务也只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A的S锁之前不能对A做任何修改。
举例:select ... in share mode,当前读,加读锁
2、写锁
概念:写锁又叫排他锁或X锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A。其他事务不能再对A加任何锁,直到T释放A上的X锁。这保证了其他事务在T释放A上的X锁之前不能对A进行读取和修改。
举例:insert/update/delete操作;select ... for update,当前读,加写锁