一次postgresql锁事件,虽然知道如何解决了,但是数据库背后的原理是什么?数据库都有哪些锁?为什么要有锁?记得数据库事务有ACID四大特性:原子性(事务是完整的操作要么成功要么回滚)、一致性(数据是一致的,银行金额一致)、隔离性(并发事务修改数据是隔离的)和持久性(事务处理的结果是持久化的)。相信你猜的出来锁应该和并发事务有关,但具体原因是什么呢?
并发事务的优缺点
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。
并发事务处理带来的4类问题:
更新丢失(Lost update) :两个事务T1和T2读入同一个数据并修改,T2提交的结果覆盖了T1提交的结果,导致T1的修改被丢失。
脏读(Dirty Reads) :事务T1对数据进行修改,但是还没有提交时,事务T2读取数据进行修改,此时T2读取的是T1修改了的值。突然由于某种原因T1进行了回滚,这时候数据恢复了原来的值,而T2取得的数据依然是T1修改的值,这就导致了数据库中的值与事务获取的值不同的现象,这就叫脏读。
不可重复读(Non-repeatable Reads) :在一个事务内,多次读取同一数据。在这个事务还没结束时,另外一个事务对该数据进行了修改。此时第一个事务再去读此数据时读到的结果与之前的结果不同。在一个事务内两次相同的查询读到的数据是不一样的,这就是不可重复读。
幻读(Phantom Reads) :目前工资为5000元的员工有10个人,事务A读取所有工资为5000元的员工人数为10人。此时事务B插入一条工资也为5000的记录。此时事务A再次读取工资为5000元的员工,记录为11人。此时产生了幻读。
不可重复读和幻读的区别:前者的重点是修改:同样条件下,你读取过的数据,再次读取出来发现值不一样了。幻读的重点在于新增或者删除:同样条件下,第一次和第二次读出来的记录数不一样。
“脏读”、“不可重复读”和“幻读”,都是数据库读一致性问题,避免不一致的方法和技术就是进行并发控制,最常用的就是封锁技术。
锁技术
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的 计算资源(如CPU、RAM、I/O等)的争用外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一 个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
按锁类型划分,可分为共享锁、排他锁
按锁的粒度划分,可分为表级锁、行级锁、页级锁
按使用机制划分,可分为乐观锁、悲观锁
共享锁(也叫写锁、S锁):多个事务可封锁一个共享页;任何事务都不能修改该页;通常是该页读取完毕,S锁立即被释放。在执行select语句的时候需要给操作对象加上共享锁,但加锁之前需要检查是否有排他锁,如果没有,则可以加共享锁(一个对象上可以加n个共享锁)。共享锁通常在执行完select语句后被释放,当然也有可能是在事务结束(包括正常结束和异常结束)的时候被释放,主要取决于数据库所设置的事务隔离级别。
排他锁(也叫写锁、X锁):仅允许一个事务封锁此页;其他任何事务必须等到X锁被释放才能对该锁页进行访问;X锁一直到事务结束才能被释放。执行insert、update、delete语句的时候需要给操作的对象加排他锁,在加排他锁之前必须确认该对象上没有其他任何锁,一旦加上排他锁之后,就不能再给这个对象加其他任何锁。排他锁的释放通常是在事务结束的时候(当然也有例外,就是在数据库事务隔离级别被设置成Read Uncommitted(读未提交数据)的时候,这种情况下排他锁会在执行完更新操作之后被释放,而不是在事务结束的时候)。
表级锁:直接锁定整张表,在锁定期间,其他进程无法对该表进行写操作。如果是写锁,则其他进程读也不允许。特点是:开销小、加锁快,不会出现死锁。锁定粒度最大,发生锁冲突的概率最高,并发度最低。
行级锁:仅对指定的记录进行加锁,其他进程还可以对同一个表中的其他记录进行操作。特点:开销大,加锁慢,会出现死锁。锁定的粒度最小,发生锁冲突的概率最低,并发度也最高。
页级锁:一次锁定相邻的一组记录。开销和加锁时间介于表级锁和行级锁之间;会出现死锁;锁定粒度也介于表级锁和行级锁之间,并发度一般。
MySQL不同的存储引擎支持不同的锁机制。比如MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
悲观锁(Pessimistic Lock):当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制。悲观锁主要是共享锁或排他锁。
乐观锁( Optimistic Locking ):相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
1) 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
2) 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
3) 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
4) 期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
要使用悲观锁,必须关闭MySQL数据库的自动提交属性。MySQL默认使用autocommit模式,当我们执行一个更新操作后,MySQL会立刻将结果进行提交。(sql语句:set autocommit=0)
乐观锁的实现主要是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
如何选择?
1) 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2) 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。
数据库事务的隔离级别
封锁技术虽然会解决数据不一致性问题,但也会造成死锁和性能下降。为了兼顾并发效率与异常控制,定义了4中隔离级别。
1、Read uncommitted(读未提交数据):如果一个事务已经开始写数据,则另外一个事务则不允许同时写操作,但允许读此行数据。该隔离级别通过“排他写锁”实现。避免了更新丢失,却可能出现脏读、不可重复读、幻读的情况。
2、Read committed(读已提交数据):读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读、幻读的情况。
3、Repeatable read(可重复读:读不写写全禁):读取数据的事务将会禁止写事务但允许读事务,写事务则禁止其他任何事务。避免了不可重复读取和脏读,但是有时可能出现幻读。这种隔离级别通过“共享读锁”和“排他写锁”实现。
4、Serializable(可串行化):提供严格的事务隔离。要求事务串行化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务串行化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。串行化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。
大多数数据库的默认级别是读已提交Read committed,比如Sql Server , Oracle。
Mysql的默认隔离级别就是可重复读。
对于多数应用程序,优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
参考