数据库锁分类和总结


一、 锁的本质与目的

锁的本质是一种协调多个会话(或事务)对共享资源进行并发访问的机制。

主要目的

维护数据一致性:确保事务在读取或修改数据时,看到的是一个一致的状态。

防止并发问题

  • 脏读:一个事务读取了另一个未提交事务的数据。

  • 不可重复读:在同一事务中,两次读取同一数据,结果不同(被其他事务修改)。

  • 幻读:在同一事务中,两次执行相同的查询,返回的结果集不同(被其他事务插入或删除)。


二、 按锁的粒度分类

锁的粒度指的是锁定的数据范围。粒度越小,并发性越好,但锁管理开销越大。

锁粒度 描述 优点 缺点 常见数据库
数据库锁 锁定整个数据库 管理简单 并发度极低,几乎不使用 所有
表锁 锁定整张表 实现简单,开销小 并发度非常低,影响其他所有行 MySQL (MyISAM), Oracle
页锁 锁定一个数据页(通常是几KB) 开销和并发性介于表锁和行锁之间 会出现页内竞争,粒度不够细 SQL Server, Sybase
行锁 锁定一行数据 并发度高,冲突概率小 开销最大,需要维护大量锁信息 MySQL (InnoDB), Oracle, PostgreSQL
意向锁 一种特殊的表级锁,表示事务即将在更细的粒度上加锁 支持多粒度锁共存,提高表级锁检查效率 - MySQL (InnoDB), Oracl

意向锁是表级锁,它"宣告"一个事务打算在表中的某些行上施加什么类型的锁。

  • 意向共享锁(IS) :事务打算给某些行加共享锁(S)

  • 意向排他锁(IX) :事务打算给某些行加排他锁(X)

作用 :当另一个事务想给整个表加锁时(例如,ALTER TABLE),它可以快速检查表上是否有意向锁,从而无需逐行检查是否有冲突的行锁,大大提高了效率。

三、悲观锁

从数据库系统角度分为三种:排他锁、共享锁、更新锁。

从程序员角度分为两种:一种是悲观锁,一种乐观锁。

3.1 悲观锁(Pessimistic Lock)

顾名思义,很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿这个数据就会block(阻塞),直到它拿锁。

悲观锁(Pessimistic Lock):正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

传统的关系数据库里用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。

悲观锁按使用性质划分

3.2 共享锁(Share Lock)

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

性质

  1. 多个事务可封锁同一个共享页;

  2. 任何事务都不能修改该页;

  3. 通常是该页被读取完毕,S锁立即被释放。

在SQL Server中,默认情况下,数据被读取后,立即释放共享锁。

例如,执行查询语句"SELECT * FROM my_table"时,首先锁定第一页,读取之后,释放对第一页的锁定,然后锁定第二页。这样,就允许在读操作过程中,修改未被锁定的第一页。

例如,语句"SELECT * FROM my_table HOLDLOCK"就要求在整个查询过程中,保持对表的锁定,直到查询完成才释放锁定。

3.3 排他锁(Exclusive Lock)

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

性质

  1. 仅允许一个事务封锁此页;

  2. 其他任何事务必须等到X锁被释放才能对该页进行访问;

  3. X锁一直到事务结束才能被释放。

产生排他锁的SQL语句如下:select * from ad_plan for update

3.4 更新锁

U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。

因为当使用共享锁时,修改数据的操作分为两步:

  1. 首先获得一个共享锁,读取数据,

  2. 然后将共享锁升级为排他锁,再执行修改操作。

这样如果有两个或多个事务同时对一个事务申请了共享锁,在修改数据时,这些事务都要将共享锁升级为排他锁。这时,这些事务都不会释放共享锁,而是一直等待对方释放,这样就造成了死锁。

如果一个数据在修改前直接申请更新锁,在数据修改时再升级为排他锁,就可以避免死锁。

性质

  1. 用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;

  2. 当被读取的页要被更新时,则升级为X锁;

  3. U锁一直到事务结束时才能被释放。

悲观锁按作用范围划分为:行锁、表锁。

行锁

锁的作用范围是行级别。

表锁

锁的作用范围是整张表。

数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

复制代码
举个例子,一个用户表user,有主键id和用户生日birthday。 

当你使用update ... where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁; 

当你使用update ... where birthday=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁。

四、乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以,不会上锁。但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号等机制。

乐观锁( Optimistic Locking ): 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。

乐观锁,大多是基于数据版本( Version )记录机制实现。

数据版本:为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "version" 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

五、乐观锁的实现方式

  • 版本号(version)

版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update ... where ... and version="old version"这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。

  • 时间戳(使用数据库服务器的时间戳)

时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。

  • 待更新字段

待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。

  • 所有字段

所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。

六、并发控制会造成两种锁

并发控制会造成活锁和死锁,就像操作系统那样,会因为互相等待而导致。

活锁

定义:指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去。

解决方法:采用"先来先服务"策略可以避免。

死锁

定义:就是我等你,你又等我,双方就会一直等待下去。比如:T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁,死锁这种没有完全解决的方法,只能尽量预防。

预防方法:

  1. 一次封锁法,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度;

  2. 顺序封锁法,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。

系统判定死锁的方法:

  • 超时法:如果某个事物的等待时间超过指定时限,则判定为出现死锁;
  • 等待图法:如果事务等待图中出现了回路,则判断出现了死锁。

对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。

相关推荐
越来越无动于衷5 小时前
SQL 拼接完全指南
数据库·sql
weixin_46686 小时前
Redis数据库基础
数据库·redis·缓存
清风6666666 小时前
基于单片机的档案库房漏水检测报警labview上位机系统设计
数据库·单片机·毕业设计·课程设计·labview·期末大作业
DarkAthena6 小时前
【GaussDB】在duckdb中查询GaussDB的数据
数据库·gaussdb·duckdb
苏琢玉6 小时前
收藏版:Phinx 数据库迁移完全指南
数据库·mysql·php
七分小魔女7 小时前
MySQL查看服务器/客户端版本
服务器·数据库·mysql
亿坊电商7 小时前
如何检查开源CMS的数据库连接问题?
数据库·开源
指针不指南吗7 小时前
【论文阅读】图数据库 Survey: Graph Databases
数据库·论文阅读
fusugongzi7 小时前
mysql管理语句
数据库·mysql