0%

数据库锁的基本原理

数据库锁的基本原理

为什么要锁

数据库通常有大量的用户在同时操作,所以并发的情况下需要控制对临界资源的操作,数据库通过锁来控制对临界资源的访问,从而保证数据的一致性。例如对于同一个账户,操作之前账户余额为 1000,同时开始 2 个事务,一个事务取款 100,一个事务往账户中汇入 100,那么 2 个事务结束后,账户的余额必须还是 1000,否则要么银行不干,要么个人不干。

锁类型

共享锁(读锁)

读锁是共享的,互相不阻塞,也就是可以多个客户同时读取同一资源,而不互相干扰。

加锁条件:当执行 select 时,数据库为这个事务分配一把共享锁,来锁定被查询的数据。

解锁条件:默认情况下,数据被读取之后,数据库立即解锁。例如 select * from table 中,先锁定第一行,读取后,立即解锁第一行,然后再锁定第二行,这样大大降低锁争用程度。在 repeatable read 和 serializable 这两种事务隔离级别下,共享锁是在事务结束时释放的,serializable 对表加锁。

排它锁(独占锁)(写锁)

写锁是排他的,一个写锁会阻塞其他的读锁和写锁,只有这样才能保证同一时刻,只有一个用户能够写入,并阻止其他用户读取正在写入的同一资源。如果要锁定的数据资源,已经放置了其他的锁,则不能再放置排它锁。
加锁条件:当执行 insert update 和 delete 语句时,数据库会对操纵的资源使用排它锁。
解锁条件:事务结束时解锁。

锁原理

锁的基本原理如下:

1.当第一个事务访问数据库资源时,如果执行 select 语句,则必须先获得共享锁,如果执行 insert update 和 delete 时,必须获得排它锁 2.当第二个事务也要访问相同的资源时,如果执行 select 语句,也必须先获得共享锁,如果执行 insert update 或者 delete,也必须获得共享锁。根据已经放置在资源上的锁类型,来决定第二个事务是应该等待第一个事务释放锁,还是立即获得锁。

资源上的锁 第二个事务进行读 第二个事务进行写
立即获得共享锁 立即获得排他锁
共享锁 立即获得共享锁 等待第一个事务释放锁
排他锁 等待第一个事务释放锁 等待第一个事务释放锁

锁粒度

常见的锁有表锁、 行锁、 页锁、外键锁等。

锁有一定的消耗,主要有:获得锁,检查锁是否已经解除,释放锁等,这些操作都会增加系统的开销。
表锁开销比较小,但是并发性能也较差,行锁并发性能高,但是需要更多锁,数据库系统一般都支持锁的自动升级,例如一个事务中的锁过多的时候,可能会将行锁升级到表锁。

mysql 的各个存储引擎根据不同的应用场景采用不同的锁机制,MyISAM 存储引擎采用表锁,InnoDB 使用行锁。如果执行 alert table 之类的操作,服务器也会采用表锁,而忽略存储引擎的锁机制。
页锁,某些数据库支持页锁,页锁粒度介于行锁和表锁之间,用于锁定存放数据的页,1 页通常含有 n 个数据行。

外键锁:外键会产生高级别的锁,后面介绍死锁的时候会提到。

乐观锁和悲观锁

悲观锁:假设如果不加锁就一定会出现问题。
乐观锁:先假定不会出现并发问题,出现问题后再采取相应的措施。
悲观锁通过select * from tbl for update实现。
乐观锁可以在表中加一个 version 字段,每次更新的时候都+1,这样如果出现并发,第二次更新的时候 version 的值已经不再匹配,可以在这时采取相应的措施。

死锁

死锁是如何产生的

同 java 的死锁一样,都是互相等待对方释放锁,造成了相互阻塞,造成的死锁。
请看下面的表格:

如何避免死锁

1.修改操作表的顺序
从上面的示例中可以看出,如果调整一下操作表的顺序就可以避免死锁。 2.外键
如果向子表写记录,那么外键约束会检查父表的记录,并锁住父表的记录,来保证这条记录不会在这个事务完成之时就被删除了。
产生高级别的锁,会阻止其他事务操作或者其他 DML 操作,如果是因为外键产生的死锁,可以去掉外键约束,由应用来保证数据的完整性,通常生产环境都不建议加外键约束。 3.短事务
缩短事务的执行时间,可以减少锁的持有时间,可以降低死锁的风险。

锁可能出现的问题

上锁是有开销的,即使不是给数据行而是给数据页上锁,也是有一定的时间的,如果第一个事务 update 一张大表,这时候第二个事务 update 这张大表比较靠后的位置的数据,这时候就可能会出现,第一个事务还没来得及给相应的数据上锁,第二个事务已经上了锁,所以出现第一个事务要等待第二个事务提交后释放锁,才能继续执行的情况。