数据库锁
锁的分类(使用方式划分)
悲观锁与乐观锁
悲观锁(Pessimistic Lock)
具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。传统的关系型数据库很多用到这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
排他锁(Exclusive Lock)
X锁,写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁。
- 仅允许一个事务封锁此页,其他任何事务必须等到排他锁被释放才能对该页进行访问;
- 排他锁一直到事务结束才能被释放
举个栗子
update table set name='h' where id='1'
update table set age='1' where id='2'
假设两个语句同时执行,前者在执行时就会给table加上X锁,那么后者执行时也想加上X锁,只是不允许的,所有必须等前者执行完,后者才能执行。
共享锁(Share Lock)
S锁,读锁,用于所有的只读数据操作。共享锁是非独占的,允许其他事务读取其锁定的资源。
- 因为是非独占的,所有允许多个事务可封锁同一个共享页,一个锁可以同时被多个线程拥有,任何事务都不能修改;
- 通常是该页被读取完毕,S锁立即被释放。
举个栗子
select * from table
select * from table
假设两个语句同时执行,同样前者在执行时会加S锁,但是他允许多个事务读取,所以后者也能执行,而不需要等待,但是若后者是update table set name='h' where id='1'
,想在table中加排他锁,那么这就是不允许的了,因为S锁只允许读取,而不允许修改。
更新锁(Update Lock)
U锁,为解决死锁,引入更新锁。更新锁的意思是:“我现在只想读,你们别人也可以读,但我将来可能会做更新操作,我已经获取了从共享锁(用来读)到排他锁 (用来更新)的资格”。
- 用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;
- 当被读取的页要被更新时,则升级为X锁;
- U锁一直到事务结束时才能被释放。
乐观锁(Optimistic Lock)
认为一般情况下数据不会造成冲突,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号、时间戳等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
版本号
- 为表中加一个 version 字段,当读取数据时,连同这个 version 字段一起读出;
- 数据每更新一次就将此值加一,当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作。
时间戳
跟版本号的原理其实类似,也是在表中添加一个 timestamp 的时间戳字段,然后提交更新时判断数据库中对应记录的当前时间戳是否与之前取出来的时间戳一致,一致就更新,不一致就重试。
引用实例
如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
3 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
锁粒度控制
如上所说,其实加各种锁都是为了保证在读取或修改数据中尽可能的不出现问题,然而加锁本身就会带来一些问题,加锁也需要消耗资源,锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销,影响系统的性能,我们要做的就是在锁的开销与数据的安全性中寻找最优解。
表级锁
锁定整个表,表锁时Mysql中最基本的锁策略,也是开销最小的,加锁快,无死锁。而他的劣势就是发生冲突的概率高,而且处理并发问题的能力低。
加锁的方式:
- 自动加锁,查询操作(SELECT),会自动给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT),会自动给涉及的表加写锁。
- 显示加锁,如
lock table tableName read;
。
页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁,优缺点相对于表锁和行锁也是折中的。页级锁仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。
行级锁
锁定某一行数据,行锁的开销大,加锁慢,并且会出现死锁;但是他的锁的粒度小,发生锁冲突的概率低,处理并发的能力强,并且在事务回滚时能减少改变的数据。
加锁的方式:
- 自动加锁,对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁。
- 显示加锁,如
select * from tableName where id = '1' for update;
。
数据库事务
四大特性
- 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。 - 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 - 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 - 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
隔离级别
- ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。
这种隔离级别会产生脏读,不可重复读和幻像读。 - ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据
- ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。 - ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
除了防止脏读,不可重复读外,还避免了幻像读。
分布式事务
分布式事务太过庞大,后期再写吧。
数据异常
丢失更新
若两个事务同时对一个字段进行更新,两个不同事务同时获得数据,然后在各自事务中同时修改了该数据,那么先提交的事务做的更新会被后提交事务的更新覆盖,这就造成了丢失更新。
脏读
若事务A正在对某个字段进行更新,同时事务B读取这个更新后的字段,并读取成功,然而事务A后面又做了回滚,那么这就产生了脏读。
不可重复读
一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次的结果不一样,那么就是产生了不可重复读。利用脏读的例子,若事务A回滚后事务B又执行了一次,那么很明显事务B两次读到的结果是不一致的。
幻读
执行某个查询操作时,事务A读的时候读出了4条记录,事务B在事务A执行的过程中 增加 了1条,事务A再读的时候就变成了 5 条,就产生了幻读。
与君共勉!!