5.28总结

发布时间 2023-06-08 17:36:31作者: 普信男孩阿洲

今天学习了数据库中锁的一些知识:

一. 数据库锁
分类:

从程序员角度分:乐观锁、悲观锁;
从数据库机制上分:共享锁、排他锁;
按照锁的粒度分:行级锁和表级锁;
数据库锁出现的原因是为了处理并发问题,因为数据库是一个多用户共享的资源,当出现并发的时候,就会导致出现各种各样奇怪的问题,就像程序代码一样,出现多线程并发的时候,如果不做特殊控制的话,就会出现意外的事情,比如“脏“数据、修改丢失等问题。

所以数据库并发需要使用事务来控制,事务并发问题需要数据库锁来控制,所以数据库锁是跟并发控制和事务联系在一起的。

平时会经常看到或者听到数据库锁有“共享锁”,“排它锁”,“互斥锁”,“写锁”,“读锁”,“悲观锁”,“乐观锁”,“行级锁”,“表级锁”,“页级锁”等,同时我们还会常看到“丢失修改“,”不可重复读“,”读脏数据“这三个术语,他们究竟是什么关系以及怎么理解呢,下面就来介绍一下他们。

先说事务的特性,要想成为事务,必须满足:ACID(原子性,一致性,隔离性,持久性)四特性,事务是恢复和并发控制的基本单位。

原子性指的是事务是数据库的逻辑工作单位,事务中操作要么都做,要么都不做;
一致性指的是事务的执行结果必须是使数据库从一个一致性状态变成另一个一致性状态,一致性和原子性是密切相关的;
隔离性指的是一个事务执行不能被其他事务干扰;
持久性指的是一个事务一旦提交,他对数据库中数据的改变就是永久性的。
从程序员角度:悲观锁和乐观锁;
并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳:

乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;

悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;
时间戳就是不加锁,通过时间戳来控制并发出现的问题。悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允许其他用户访问那部分数据。乐观锁就比较简单了,就是不做控制,这只是一部分人对于并发所持有的一种态度而已。时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量,因为这种方法可以避免了长事务中的数据库加锁开销(操作员A 和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。
以上悲观锁所说的加“锁”,其实分为几种锁,分别是:排它锁和共享锁,其中排它锁又称为写锁,共享锁又称为读锁。(ps.可以参考此文 “http://blog.sina.com.cn/s/blog_548bd2090100ir7k.html”)

悲观锁划分:共享锁和排它锁是具体的锁,是数据库机制上的锁
共享锁(Share Lock)

S锁,也叫读锁,用于所有的只读数据操作。共享锁是非独占的,允许多个并发事务读取其锁定的资源。 
性质: 

多个事务可封锁同一个共享页; 
任何事务都不能修改该页; 
通常是该页被读取完毕,S锁立即被释放。
排他锁(Exclusive Lock)

X锁,也叫写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。(某个顾客把试衣间从里面反锁了,其他顾客想要使用这个试衣间,就只有等待锁从里面打开了。) 
性质:

仅允许一个事务封锁此页; 
其他任何事务必须等到X锁被释放才能对该页进行访问; 
X锁一直到事务结束才能被释放。
存在以下关系:

 

                                        (x表示是排它锁(Exclusive),s表示共享锁(Share),Y表示yes,N表示no)

上图表示可以共存的锁,如,第二行表示,一个事务T1给某数据加了X锁,则事务T2就不能再给那数据加X锁了,同时也不能再加S锁了,只有到T1事务提交完成之后,才可以。默认来说,当sql脚本修改更新某条记录的时候,会给该条记录加X锁,读的话加的是S锁。

另外,并发操作会导致数据的不一致性,主要包括“丢失数据”,“不可重复读”,“读脏数据等。(详细可看 王珊 版 数据库系统概论第十一章 并发控制)。

还有就是,并发控制会造成活锁和死锁,就像操作系统那样,会因为互相等待而导致。活锁指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去,这种处理方法就是采用“先来先服务”策略;死锁就是我等你,你又等我,双方就会一直等待下去,比如:T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁,死锁这种没有完全解决的方法,只能尽量预防,预防的方法有:①一次封锁发,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度;②顺序封锁发,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。另外,系统如何判断出现死锁呢,毕竟出现死锁不能一直干等下去,要及时发现死锁同时尽快解决出现的死锁,诊断和判断死锁有两种方法,一是超时法,二是等待图法。超时法就是如果某个事物的等待时间超过指定时限,则判定为出现死锁;等待图法指的是如果事务等待图中出现了回路,则判断出现了死锁。对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。

最后,说下行级锁和表级锁。(一下摘自:http://blog.sina.com.cn/s/blog_548bd2090100ir7k.html)

 

按照锁的粒度分:行级锁和表级锁、页级锁
行级锁是一种排他锁,防止其他事务修改此行;

开销大,加锁慢,会出现死锁,锁定力度最小,发生锁冲突的概率最低,并发度高;

在使用以下语句时,Oracle会自动应用行级锁:

INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];

SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新;

使用COMMIT或ROLLBACK语句释放锁;

表级锁

开销小,加锁快,不会出现死锁,锁定力度大,发生冲突所的概率高,并发度低;

又分为5类:

行共享 (ROW SHARE) – 禁止排他锁定表;

行排他(ROW EXCLUSIVE) – 禁止使用排他锁和共享锁;

共享锁(SHARE) - 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁;

共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止使用共享锁及更高的锁;

排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表;

页级锁:

开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定力度介于表和行行级锁之间,并发度一般。

二. 关于事务一致性
事务是一个高度抽象的概念。事务要解决的问题是如何使多次操作,对外部看起来是一个整体的操作。理想的事务是一把锁,把一个一个的事务按照队列一样排列,不过这样性能我们无法接受,因此引入了引入了MVCC (Multi-Version Concurrency Control) 的概念,MVCC本质是copy-on-write。

事务的本质是什么?事务=锁+MVCC

怎么理解一致性:

一致性是指数据处于一种语义上的有意义且正确的状态。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其他事务不可见的。因为这些中间状态,是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。
举个栗子,张三给李四转账100元。事务要做的是从张三账户上减掉100元,李四账户上加上100元。一致性的含义是其他事务要么看到张三还没有给李四转账的状态,要么张三已经成功转账给李四的状态,而对于张三少了100元,李四还没加上100元这个中间状态是不可见的。
那么反驳的声音来了:

要么转账操作全部成功,要么全部失败,这是原子性。从例子上看全部成功,那么一致性就是原子性的一部分咯,为什么还要单独说一致性和原子性?

你说的不对。在未提交读的隔离级别下是事务内部操作是可见的,这时候会出现脏读,明显违背了一致性,怎么解释?

好吧,你的疑问很有道理,也很充分,这正说明你对事务的ACID特性理解的很到位。不过,需要注意的是:

原子性和一致性的的侧重点不同:原子性关注状态,要么全部成功,要么全部失败,不存在部分成功的状态。而一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见。

在未提交读的隔离级别下,会造成脏读,这就是因为一个事务读到了另一个事务操作内部的数据。ACID中事务一致性描述的是一个最理想的事务应该怎样的,是一个强一致性状态,如果要做到这点,需要使用排它锁把事务排成一队,即Serializable的隔离级别,这样性能就大大降低了。现实是骨感的,所以使用隔离性的不同隔离级别来破坏一致性,来获取更好的性能。