ACID

Mysql锁和事务

其实AC是一个概念,就是要么一起执行,要么都不执行,只是看问题的指标不同而已,一个侧重过程,一个侧重结果

  • A:原子性,联级操作,要么一起执行,要么一起回滚,不存在执行了操作一,但是操作二失败了,操作一、二是一个整体
  • C:一致性,要么处于修改都成功,要么处于修改都失败,一致性的状态。(可以指单节点的一个事务下的系列操作,也可以指集群状态下所有节点的数据状态,比如zk集群,又分强一致性,弱一致性,最终一致性)
  • I:隔离性,比如A 和 B同时开启事务,A操作了数据a,B读取a会读取之前的a,而不是刚被A操作的a(好的隔离性可以避免:脏读、不可重复读、幻读),目前只有串行化才能实现彻底的事务隔离
  • D:持久性,提交过的事务,会持久性的保存在数据库当中,即使宕机还有效
  1. SELECT @@tx_isolation;
  2. set transaction isolation level read uncommitted;
  3. set transaction isolation level read committed;
  4. set transaction isolation level repeatable read;
  5. set transaction isolation level serializable;

并发容易引发的问题

  • 第一类丢失更新(Update Lost):此种更新丢失是因为回滚的原因,所以也叫回滚丢失。此时两个事务同时更新count,两个事务都读取到100,事务一更新成功并提交,count=100+1=101,事务二出于某种原因更新失败了,然后回滚,事务二就把count还原为它一开始读到的100,此时事务一的更新就这样丢失了。
  • 脏读(Dirty Read):此种异常时因为一个事务读取了另一个事务修改了但是未提交的数据。举个例子,事务一更新了count=101,但是没有提交,事务二此时读取count,值为101而不是100,然后事务一出于某种原因回滚了,然后第二个事务读取的这个值就是噩梦的开始。
  • 不可重复读(Not Repeatable Read):此种异常是一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果,也就是在一个事务里面你不能重复(即多次)读取一行数据,如果你这么做了,不能保证每次读取的结果是一样的,有可能一样有可能不一样。造成这个结果是在两次查询之间有别的事务对该行数据做了更新操作。举个例子,事务一先查询了count,值为100,此时事务二更新了count=101,事务一再次读取count,值就会变成101,两次读取结果不一样。
  • 第二类丢失更新(Second Update Lost):此种更新丢失是因为更新被其他事务给覆盖了,也可以叫覆盖丢失。举个例子,两个事务同时更新count,都读取100这个初始值,事务一先更新成功并提交,count=100+1=101,事务二后更新成功并提交,count=100+1=101,由于事务二count还是从100开始增加,事务一的更新就这样丢失了。
  • 幻读(Phantom Read):幻读和不可重复读有点像,只是针对的不是数据的值而是数据的数量。此种异常是一个事务在两次查询的过程中数据的数量不同,让人以为发生幻觉,幻读大概就是这么得来的吧。举个例子,事务一查询order表有多少条记录,事务二新增了一条记录,然后事务一查了一下order表有多少记录,发现和第一次不一样,这就是幻读。

事务级别(默认 REPEATABLE-READ,可能会幻读的)

  • 读未提交(Read Uncommitted):该隔离级别指即使一个事务的更新语句没有提交,但是别的事务可以读到这个改变,几种异常情况都可能出现。极易出错,没有安全性可言,基本不会使用。
  • 读已提交(Read Committed):该隔离级别指一个事务只能看到其他事务的已经提交的更新,看不到未提交的更新,消除了脏读和第一类丢失更新,这是大多数数据库的默认隔离级别,如Oracle,Sqlserver。
  • 可重复读(Repeatable Read):该隔离级别指一个事务中进行两次或多次同样的对于数据内容的查询,得到的结果是一样的,但不保证对于数据条数的查询是一样的,只要存在读改行数据就禁止写,消除了不可重复读和第二类更新丢失,这是Mysql数据库的默认隔离级别。
  • 串行化(Serializable):意思是说这个事务执行的时候不允许别的事务并发执行.完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离的最高级别,虽然最安全最省心,但是效率太低,一般不会用。

事务A

  1. start transaction;
  2. select * from user_t lock in share mode;

事务B

  1. start transaction;
  2. INSERT user_t values(null,'huangnew','huangnew',50);

因为隔离级别是 可串行化,事务A查询的时候把所有的查询行都加了共享锁,所以事务B阻塞,从而避免了幻读的可能性。

锁机制

  1. 共享锁(S) 和排他锁(X)
  • 共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
  • 不带任何锁的普通查询,不管有没有锁,直接读老数据。
    排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制
  • 开启事务未提交的DML操作都会自动加排他锁,这个时候只能进行不带锁的普通查询,而且只能查到老数据
  1. 意向锁: 针对Innodb的优化,存在行锁的时候,对表中某一行或者几行数据加锁的时候,都会自动给表加表级别的 IS IX锁,这样,事务B要给表加表锁的使用因为该表以及是IS 或者IX了,所以直接加锁失败,而不需要遍历表中的所有记录是否有锁。意向共享锁(IS)、意向排他锁(IX)

  2. 间隙锁:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:Select * from emp where empid > 100 for update; 是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记 录并不存在)的“间隙”加锁。

Innodb行级锁:共享锁(S)、排他锁(X)
Innodb表级锁:意向共享锁(IS)、意向排他锁(IX)、手动Lock

直接加表锁:

  1. lock tables t1 write|read;
  2. UNLOCK TABLES;
  3. SELECT * FROM `user_t` for update;
  1. 悲观锁、乐观锁
  • 悲观锁
    特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。
    select … for update操作来实现悲观锁,并发进行的时候,下一个事务如果有行交集的话就会阻塞。
    也是串行化级别的采取的策略

悲观锁的特点是先获取锁,在进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的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中用悲观锁务必要确定走了索引,而不是全表扫描。

  • 乐观锁
    乐观锁的区别在于乐观的认为获取锁是很有可能成功的,如果真的不成功,被别人改过数据了,则回滚。
    乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳
    乐观锁 + MYISAM 可以实现带事务控制的同时支持 高性能读取,又支持不频繁带事务控制的写。适用读多写少,且回滚开销不是特别大的场景。InnoDB就是乐观锁+版本控制管理,才实现的高并发的RR事务级别。
  1. 1. SELECT data AS old_data, version AS old_version FROM …;
  2. 2. 根据获取的数据进行业务操作,得到new_datanew_version
  3. 3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
  4. if (updated row > 0) {
  5. // 乐观锁获取成功,操作完成
  6. } else {
  7. // 乐观锁获取失败,回滚并重试
  8. }

区别

  • 乐观锁是否在事务中其实都是无所谓的,悲观锁一定要有事务控制,
  • 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,特别是多个DML一起操作,所有执行成功的DML都要回滚,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。因为锁表锁行是需要数据库开销的,即使未阻塞,也是有开销的。但是每次计算都是有效的,不存在计算之后数据回滚的风险。
  • 悲观锁适合高并发,锁竞争激烈的业务场景,至少可以保证一次有1个成功,争取锁失败会等待。而乐观锁更适合竞争没有那么激烈的业务场景,减少了锁的开销,更轻量,但是竞争激烈时,会反复的修改数据失败。

死锁

  • 死锁一般发生在表和表之间,其实也可以发生在行于行之间。
  • Mysql自带等待锁超时的时间,默认50s,超时自动释放锁
  • 事务A占用了 数据1 准备更新数据2时 发现事务B已经占用了数据2,所以事务A等待事务B解锁数据2.
    事务B占用了数据2准备更新数据1时,发现事务A已经占用了数据1,所以事务B等待事务A解锁诗句1.
    这个时候就是死锁了。

如何避免死锁:业务上做优化,或者在并发度上做妥协

  1. 尽量用行锁别用表锁,为表添加合理的索引。如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。
  2. 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小,可以提交的独立提交。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 以固定的顺序访问表和行。即按顺序申请锁,这样就不会造成互相等待的场面。编程的时候要注意。
  5. 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。