Mysql事务和锁

ACID

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

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

并发容易引发的问题

  • 更新丢失:同时修改同一数据,A先修改完,B再修改,A的修改结果丢失。

  • 脏读:更改但未提交的数据,被其他人查询到了未提交的数据,脏读

  • 不可重复读:一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。但是可能会查到之前不存在的数据,幻读。

  • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”
    幻读案例:
    例如在RR级别下,开启一个事务A,查询数据,开启一个数据B,插入新数据并且提交,name事务A虽然直接查询不到新数据,因为可重复读,但是可以update插入的新数据,并且update成功后,可以查到新数据。太神奇了。

脏读 和 不可重复读是两回事,禁止脏读,就是只读取提交后的数据,无法避免,一个事务第一次读数据还没结束事务,该数据被其他事务读取且删除而且提交了,再次读取就读取不到或者读取不一致,还是会发生不可重复读的情况。

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

  • READ UNCOMMITTED ,可以读到其他事务修改甚至未提交的,-->引发脏读
  • READ COMMITTED ,其他事务对数据库的修改,只要已经提交,其修改的结果就是可见的,与这两个事务开始的先后顺序无关-->不可重复读,在一个事务中两次读取不一致
  • REPEATABLE READ,可重复读,完全适用MVCC,只能读取在它开始之前已经提交的事务对数据库的修改,在它开始以后,所有其他事务对数据库的修改对它来说均不可见
  • Serializable(可串行化):一条数据加锁了之后,其他事务既无法读取也无法修改一条数据,牺牲性能换数据安全性

事务A

start transaction;
select * from user_t lock in share mode;

事务B

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

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

  • RR不能解决幻读的解释
    因为可重复读和提交读本身就是冲突的,默认可重复读是会出现幻读的,除非加共享锁取读取,就可以读取提交的最新的数据。
    只有串行化才可以保证数据的绝对不冲突。

锁机制

  1. 共享锁 和排他锁
    https://www.cnblogs.com/boblogsbo/p/5602122.html
  • 共享锁又称为读锁,简称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了,所以直接加锁失败,而不需要遍历表中的所有记录是否有锁。
  1. mysql的行级锁
    开始事务update一条数据的话,可以用共享锁 和 排他锁的方式读取非这条数据
  • 锁命令
lock tables t1 write|read;
UNLOCK TABLES;
SELECT * FROM `user_t` for update;

  1. 悲观锁、乐观锁
    https://www.cnblogs.com/zhiqian-ali/p/6200874.html
  • 悲观锁
    特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。
    select … for update操作来实现悲观锁,并发进行的时候,下一个事务如果有行交集的话就会阻塞。
    也是串行化级别的采取的策略

  • 乐观锁
    乐观锁的区别在于乐观的认为获取锁是很有可能成功的,如果真的不成功,被别人改过数据了,则回滚。
    乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳
    乐观锁 + MYISAM 可以实现带事务控制的同时支持 高性能读取,又支持不频繁带事务控制的写。适用读多写少,且回滚开销不是特别大的场景。InnoDB就是乐观锁+版本控制管理,才实现的高并发的RR事务级别。

1. SELECT data AS old_data, version AS old_version FROM …;
2. 根据获取的数据进行业务操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
    // 乐观锁获取成功,操作完成
} else {
    // 乐观锁获取失败,回滚并重试
}

区别

  • 乐观锁是否在事务中其实都是无所谓的,悲观锁一定要有事务控制,
  • 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,特别是多个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锁造成的死锁。

MVCC

InnDB引擎的原理,不能避免幻读,特别是UPDATE操作,能解释为什么更新并发提交后,可以查看到新增加的数据,因为插入了一行新数据,且这个新数据的CTX=当前事务ID,所以更新之后就可以查的到了。

  1. INSERT 新增的行往C里写入当前系统版本号。 这样新事物可以通过这个版本号保证不查到他

  2. DELETE 为删除的行写D字段为当前系统版本号。

  3. UPDATE 插入一行新数据写C为当前系统版本号,老数据写D为当前系统版本号。

  4. SELECT 的时候只查 C<=当前版本 && (D > 当前版本 || D is not defined) 这样主要是为了在不加锁的情况下,一个事务能够读取到事务开始前已经存在且未被删除,且没有经过修改的数据。 也就是解决脏读的问题。即该事物开始之后的其他事务的各种修改事务提交都不会对当前事务的读取产生影响。

猜你喜欢

转载自blog.csdn.net/weixin_34210740/article/details/87513122