参考:https://blog.csdn.net/weixin_41922289/article/details/88882325
参考:https://blog.csdn.net/weixin_39651041/article/details/79985715
参考:https://blog.csdn.net/C_J33/article/details/79487941
最后也给大佬级的文章总结:https://blog.csdn.net/mysteryhaohao/article/details/51669741,整理的非常全
目录
观锁和悲观锁的用处
保证数据安全,处理高并发访问;
乐观锁
概念:乐观锁就如同他的名字一样,当其他人(线程)去取数据的时候,总是认为别人不会修改数据,总不会发生并发问题,所以因此没有上锁,只有在线程提交数据时会通过检查版本号的形式检测数据有没有被修改过。一般会在数据表中添加版本号(Version)字段来表示被修改的次数,当数据被修改,version+1,只有在version字段和当前数据库的version值相同时,才提交成功
理解:谁都可以修改,修改完了提交的时候,判断版本号(一般情况),相同则修改,不同则驳回;
实现:大多数基于数据版本(Version)记录机制实现
特点:总是假设是最好的情况
乐观锁的4种实现方式:
1、版本号(version)
版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。
2、时间戳(使用数据库服务器的时间戳)
时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。
3、待更新字段
待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。
4、所有字段
所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。
悲观锁
概念:悲观锁和乐观锁就是相反的存在,他总是认为别人(别的线程)会对数据进行修改,所以都会加锁(读锁,写锁等等)一次只能允许一个线程对数据进行修改,其他线程会被阻塞挂起, java中的synchronized关键字的实现思想就是悲观锁
理解:我上了锁,那就谁都不能在我解锁之前修改;
实现:大多数情况下依靠数据库的锁机制实现
特点:总是假设是最坏的情况
乐观锁和悲观锁的区别
a. 加锁时机不同:
- 悲观锁,从数据开始修改时就将数据锁住,直到更改完成才释放锁
- 乐观锁,直到数据修改完准备提交时才上锁,完成后释放
b.并发性区别
- 因为悲观锁是在事务执行中加锁,当并发量高时,就有可能会对其他事务进程造成影响,造成其他事务进程执行时间过程,导致事务超时;
- 乐观锁是在对数据进行检查时才加锁,锁的时间会少很多,而只有锁住数据的时候会影响其它事务。
- 两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适;
排它锁(写锁)
- 加锁与解锁:当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁。如果该数据资源已经有其他锁(任何锁)存在时,就无法对其再放置独占锁了。
- 兼容性:独占锁不能和其他锁兼容,如果数据资源上已经加了独占锁,就不能再放置其他的锁了。同样,如果数据资源上已经放置了其他锁,那么也就不能再放置独占锁了。
- 并发性能:最差。只允许一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待。
概念:如果事务T对数据对象A加上写锁,那么只允许事务T读取和修改对象A,其他事务都不能在对A加任何类型的锁,直到事务T释放当前写锁
意义:保证了事务T在释放在数据对象A的锁前其他事务无法读取和修改数据对象A
共享锁(读锁)
- 加锁与解锁:当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。在默认情况下,数据被读取后,数据库系统立即解除共享锁。例如,当一个事务执行查询“SELECT * FROM accounts”语句时,数据库系统首先锁定第一行,读取之后,解除对第一行的锁定,然后锁定第二行。这样,在一个事务读操作过程中,允许其他事务同时更新accounts表中未锁定的行。
- 兼容性:如果数据资源上放置了共享锁,还能再放置共享锁和更新锁。
- 并发性能:具有良好的并发性能,当数据被放置共享锁后,还可以再放置共享锁或更新锁。所以并发性能很好。
概念:如果事务T对数据对象A加上读锁,那么其他事务只能再对数据对象A加读锁,而不能加写锁,知道事务T释放在A上的读锁
意义:保证了其他事务可以读取数据对象A,但是在释放锁之前不能对A进行任何修改
相容矩阵:
更新锁
U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。
因为当使用共享锁时,修改数据的操作分为两步:
1. 首先获得一个共享锁,读取数据,
2. 然后将共享锁升级为排他锁,再执行修改操作。
这样如果有两个或多个事务同时对一个事务申请了共享锁,在修改数据时,这些事务都要将共享锁升级为排他锁。这时,这些事务都不会释放共享锁,而是一直等待对方释放,这样就造成了死锁。
如果一个数据在修改前直接申请更新锁,在数据修改时再升级为排他锁,就可以避免死锁。
更新锁在的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象。
例如,对于以下的update语句:
UPDATE accounts SET balance=900 WHERE id=1
更新操作需要分两步:读取accounts表中id为1的记录 –> 执行更新操作。
如果在第一步使用共享锁,再第二步把锁升级为独占锁,就可能出现死锁现象。例如:两个事务都获取了同一数据资源的共享锁,然后都要把锁升级为独占锁,但需要等待另一个事务解除共享锁才能升级为独占锁,这就造成了死锁。
更新锁有如下特征:
- 加锁与解锁:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁。
- 兼容性:更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但是最多放置一把更新锁。这样,当多个事务更新相同的数据时,只有一个事务能获得更新锁,然后再把更新锁升级为独占锁,其他事务必须等到前一个事务结束后,才能获取得更新锁,这就避免了死锁。
- 并发性能:允许多个事务同时读锁定的资源,但不允许其他事务修改它。
性质
1. 用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;
2. 当被读取的页要被更新时,则升级为X锁;
3. U锁一直到事务结束时才能被释放。
活锁
概念:如果事务A封锁了数据对象R,事务B请求封锁R,于是事务B就等待事务A释放锁,事务C也请求封锁R,但是事务A在释放在R上的锁后,系统首先批准事务C封锁R,事务B就只能继续等待,紧接着,事务D也请求封锁R,事务C在释放锁后,系统又允许D对R加锁,事务B只能继续等待,这种情况就是活锁,永远的等待下去。
解决策略:可以采用类似优先队列的方式去解决,当多个事务请求封锁统一数据对象时,系统根据请求的先后顺序对这些事务排队,当数据对象上的锁一旦释放,就首先批准队列中第一个加锁。
死锁
概念:如果两个事物分别锁定了两个单独的对象,这时每一个事务都要求在另一个事务锁定的对象上获得一个锁,因此,每一个事务都必须等待另一个事务释放当前占有的锁;
当发生死锁现象时,系统可以自动检测到,然后通过自动取消其中一个事务来结束死锁,根据事务处理时间长短来确定事务的优先级,处理时间长的事务优先级较高,当发生冲突时,保留优先级高的事务,取消优先级低的事务
解决策略:预防死锁就要破坏产生死锁的条件
1.一次封锁法:要求每个事务都必须一次将所有要使用的数据全部加锁,否则就不能继续执行;
缺点:
- 一次就将全部数据加锁,扩大了封锁的范围,降低了系统的并发量;
- 数据库中的数据总是不断变化的,很难事先确定每个事务需要加锁的数据对象,只能扩大加锁范围,但是这样会降低并发度;
2.顺序封锁法:预先对数据进行规定一个封锁顺序,所有事物按照顺序封锁;
缺点:
1.数据对象变化不定,维护成本高
意向锁:
为了表锁和行锁而存在的意向锁
官方文档中是这么描述的,
Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table
知乎上有个解释十分形象,如下:
1、在mysql中有表锁,读锁锁表,会阻塞其他事务修改表数据。写锁锁表,会阻塞其他事务读和写。
2、Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
3、这两中类型的锁共存的问题考虑这个例子:事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。
在意向锁存在的情况下,上面的判断可以改成
- step1:不变
- step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
最后也给大佬级的文章总结:https://blog.csdn.net/mysteryhaohao/article/details/51669741,整理的非常全
来源:CSDN
作者:蒂法洛克
链接:https://blog.csdn.net/inflaRunAs/article/details/103454789