闲聊Mysql的锁与事务和java中的锁机制

主要参考文章:

https://tech.meituan.com/innodb-lock.html

http://blog.csdn.net/soonfly/article/details/70238902

这篇文章主要侧重Innodb引擎~

Innodb引擎为了保证事务的一致性、隔离性以及数据在并发读-读、读-写、写-写的情况下的正确性,用到的技术有:悲观锁(表锁、行锁、GAP间隙锁、next-key锁(行锁+GAP锁))、MVCC(快照读、当前读)、乐观锁(MVCC+行锁 或 MVCC+CAS)。

为了解决表锁与行锁的冲突,Innodb还引入了意向锁。考虑以下场景:某张表有一亿条数据,事务A为某一行数据加了写锁,此时事务B想要为这张表加表级锁,此时显然是不能成功的,因为事务A持有了一个行锁。可是事务B要怎样检测到有行级锁被其他事务持有了呢,难道要扫描一亿条数据么?这样效率就低爆了,因此引入了意向锁,每当一个事务需要持有锁时(共享锁或者排他锁),首先在表上加一个意向共享锁或意向排它锁。当另外的事务想要获取表级锁时,首先检测表级锁是否被持有了,如果没被持有,则检测是否有表级意向锁,如果有,表明其他事务正在持有表中某些行的锁,因此该事务申请表锁的动作被阻塞,从而避免了全表锁检测。

参考下文:

作者:尹发条地精
链接:https://www.zhihu.com/question/51513268/answer/127777478
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

①在mysql中有表锁,LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其他事务修改表数据。LOCK TABLE my_table_name WRITe; 用写锁锁表,会阻塞其他事务读和写。②Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。③这两中类型的锁共存的问题考虑这个例子:事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。数据库要怎么判断这个冲突呢?step1:判断表是否已被其他事务用表锁锁表step2:判断表中的每一行是否已被行锁锁住。注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下,上面的判断可以改成step1:不变step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。

其实,不管是乐观锁还是悲观锁,我认为都是需要在一个变量上打标记,根据打标记是否成功来决定是否获取到了锁,因此我认为悲观锁在打这个标记的时候也是要使用cas硬件级别的指令来操作的,不同的是,悲观锁获取不到锁后线程将会阻塞住,失去cpu时间片,而乐观锁则是继续不断尝试获取标记,不失去cpu时间片,减少了线程上下文切换开销,但是增加了cpu负担。

悲观锁:从名字来看它非常悲观,在操作数据时总是认为在同一时刻会有其他人与它竞争数据,因此为了保险起见,在操作数据之前就给数据加上一把锁,不让别人操作此数据,在一定程度上保证自己对数据的独占性,比如Innodb经典的行级锁中的读锁(阻塞其他写),写锁(阻塞其他读和写)。悲观性主要体现在每次操作都要加锁,不论是否会有冲突。

乐观锁:从名字来看它很乐观,对操作数据总是持有一种积极的态度,并不会每次操作数据时都会上锁,主要有两种实现方式:
第一种是CAS,主要解决的是写-写之间的冲突,当一个写操作操作数据时,不会加锁,而是首先获取数据的值,对数据修改完之后,再去获取该数据的值,如果与第一次获取的值相同,则认为在这一小段时间内没有其他写操作与之冲突,那么就将修改后的值写入到该数据中(存在ABA问题),其中第二次的操作被称为compare and set,这个操作需要是一个原子操作,需要硬件层面的支持(目前绝大多数硬件均支持cas原子操作);
第二种是MVCC,即多版本并发控制,在Innodb引擎中,主要解决的是读-写之间的冲突,属于无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作和写操作不会互相阻塞。

Innodb中MVCC分为当前读和快照读,快照读,即读取的是与当前事务相关联的快照版本,不阻塞其他写;当前读,读取的是数据的最新版本,
需要为该记录加行锁(有可能是写锁,写锁阻塞其他写和读;有可能是共享读锁,共享读锁阻塞其他写),也有可能会配合CAS,使用乐观锁,这取决与数据库引擎在不同事务隔离级别的实现方式。

MVCC在Innodb的 在read commit以上级别 才会启用,对应到sql语句,如下:

快照读:
select * from table ....;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。

select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;

接下来说一下Innodb相对于MyISAM引擎的一大特性:事务(另外一个特性为细粒度行锁)。

mysql的 RU:没有快照读;select直接读最新的数据,而且还能读取到其他事务还未commit的数据,因此会有脏读、不可重复读、幻读问题。如果要强行使用当前读,会加行锁,因此能避免不可重复读,但是不加gap锁,存在幻读。

mysql的 RC:不好说,不好下结论,说下现象吧。单纯select(不敢说是快照读):事务B update了数据,事务A两次单纯select查不到更新,避免了脏读,但是当事务B commit后,事务A将会看到事务B update后的数据,存在不可重复读。事务B insert也会有相同的效果,存在幻读 当前读:加行锁,避免不可重复读,但是却不加gap锁,因此存在幻读的情况。

mysql的 RR:这个级别单纯select就是完全的快照读了,即MVCC,它只读取比它自己事务版本号小的数据行,避免了脏读、不可重复读,但是避免不了幻读,因为另一个事务在插入和删除的时候会在数据行上标记一个更大的事务版本号。
当前读,行锁+gap锁,避免了不可重复读和幻读!
在insert/delete时,如果能用where条件中的索引定位到数据,那么在该索引所在的区间加GAP锁,防止幻读发生(比行锁粒度大一些);如果不能用索引定位数据,那么需要加表锁,之后Innodb引擎会过滤掉不符合条件的行,并执行unlock操作释放锁(这违反了数据库事务二段封锁的规范)。

当前读的加锁问题,见 http://hedengcheng.com/?p=771

mysql的 Serializable:这个时候的单纯select加了共享锁,行锁、gap锁都用上了。

mysql的单纯select当前读
RU脏读、不可重复读、幻读幻读
RC不可重复读、幻读幻读
RR幻读
Serializable

具体解释请看 https://tech.meituan.com/innodb-lock.html

http://blog.csdn.net/fei33423/article/details/46731891

间隙锁是一个虚无缥缈的锁,因为实际索引并不存在。它的x锁和s锁是一样的,或者说,不分x和s。当一个事务a持有了一段间隙锁,另一个事务b还是可以拿到的(update、delete、select for update等),只有在这段间隙中insert才会阻塞。间隙锁不包括实际存在的行锁,行锁+间隙锁统称为next-key锁。

在serializable这个级别,锁机制就很暴力了,普通select都会加共享锁,具体操作为事务A单纯select表(行锁或表锁),事务B就无法对加了锁的数据进行写操作了。

类比于jdk中的ConcurrentHashMap,chm中的锁分段技术与数据库行锁有着异曲同工之妙,都是为了减少写-写之间的冲突,不过chm的锁分段侧重于写-写,数据库行锁既减少了写-写冲突,也减少了读-写冲突。chm的锁采用的是ReentrantLock,其基本原理也是CAS。另外chm 比较特殊 的一点是将Entry中的value值声明为volatile类型,volatile的内存可见性的语义保证了读请求读到的值永远是最新的,正确的值,而不是一个不会更新(update)而有可能造成死循环的值或破碎(broken jvm规范不强制对long和double等64位类型的读写为原子性)的值。而Innodb在read uncommitted隔离级别情况下,读不加锁,尚不清楚会不会产生脏数据。

由于Innodb采取的是二段封锁协议,事务开始后在需要的时候获取锁,在事务提交或回滚时统一释放所有持有的锁,因此持续时间较长的事务会严重加重锁竞争,导致其他事务阻塞住,数据库的吞吐量和响应时间都会被拖慢,因此在写sql语句时一定要注意,查询语句尽量单表,不要用join,增删改也尽量缩小,保证事务能尽快结束,释放占有的锁资源。

对于synchronized,可以在对象上、方法上或类对象上加锁,来实现线程对执行代码的排他性。对于在对象上加锁来说,synchronized实现如下:

java编译器会在加了对象锁的代码段前加上monitorenter字节码指令,在代码段后加上monitorenter字节码指令。
然后每个对象都有一个监视器锁,具体信息一般标记在对象头中,执行monitorenter时,首先线程尝试获取对象的监视器锁,如果监视器锁的进入数为0,则该线程进入monitor,并将进入数置为1,并将自己标记为该锁的持有者;如果该线程已经占有该monitor,只是重新进入,那么把进入数加一;如果其他线程已经占用了monitor,则该线程进入阻塞状态,进入一个阻塞队列中进行等待,直到monitor的进入数为0,再由jvm调度重新尝试获取monitor的所有权。其中在修改线程进入次数时jvm同样使用CAS。

参考 http://www.cnblogs.com/paddix/p/5367116.html

另外,synchronized是非公平锁,它无法保证等待锁的线程获取锁的顺序,非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁,导致 饥饿

有个有趣的现象是,当一个线程A通过syn获取到对象锁,执行过程中,有多个线程尝试获得锁,因线程A没有释放锁,这多个线程只能放到阻塞队列中进行等待,等A释放锁后,总是最后一个尝试获取锁的线程首先获得锁,并且获得锁的顺序为后到先得,似乎jvm实现了一个阻塞栈…

另外notifyAll也是按照栈结构的后进先出顺序唤醒线程的…

本以为jvm会按照线程请求锁的顺序或者线程优先级来进行调度,结果是后进先出这种极度不公平的做法。java并发编程实战对此的解释是,当执行加锁操作时,公平性将由于在挂起线程和恢复线程时存在的开销而极大的降低性能。

关于ReentrantLock与syn的比较,
1. syn是非公平锁,rl通过参数设置,既可以成为公平锁,也可以成为非公平锁;
2. 通过syn无法中断一个正在等待获取锁的线程,导致其会无限的等待下去,造成饥饿,甚至造成死锁,而恢复程序的唯一方法是重新启动程序。rl则提供了tryLock()非阻塞方法,如果能获取到锁或是重入,那么立即返回true;否则立即返回false。另外还提供了带超时参数的tryLock(long timeout, TimeUnit unit)方法,如果在指定时间内没有获得锁,则放弃继续争夺锁。
3. 另外rl提供了可中断的获取锁的方法lockInterruptibly(),这样当前获取锁的线程便可以响应thread.interrupt()的中断异常了。

    原文作者:java锁
    原文地址: https://blog.csdn.net/joenqc/article/details/75194104
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞