日期:2014-05-16  浏览次数:20405 次

并发编程(四):也谈谈数据库的锁机制

首先声明,本次文章基本上都是从其他人的文章中或者论坛的回复中整理而来。我把我认为的关键点提取出来供自己学习。所有的引用都附在文后,在这里也就不一一表谢了。

第二个声明,我对于Internel DB并没有研究过,所使用的也是简单的写写SQL,截止到现在最多的一个经验也就是SQL的性能调优,具体点就是通过Postgresql的执行计划,来调整优化SQL语句完成在特定场景下的数据库调优。对于锁,由于数据库支持的锁机制已经能够满足平时的开发需要。因为所从事的行业并不是互联网,没有实时性高并发的应用场景,因此也没有速到过数据库的复杂问题;对于线上应用的死锁问题,那更是没有研究过了。本文算是自己学习数据库锁机制的一个读书笔记。再次感谢各位同仁的分享。

    锁机制为什么是数据库非常重要的内容,那么看一下数据库并发的问题你就知道为什么了:

1. 数据库并发的问题

数据库带来的并发问题包括:   

   1. 丢失更新。

   2. 未确认的相关性(脏读)。

   3. 不一致的分析(非重复读)。

        4. 幻像读

详细描述如下:

1.1.丢失更新

当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。   

e.g.事务A和事务B同时修改某行的值,

  1. 事务A将数值改为1并提交
  2. 事务B将数值改为2并提交。

这时数据的值为2,事务A所做的更新将会丢失。

看下面一段sql:

select old_attributes  from table where primary_key = ? ---step1
attributes = merge(old_attributes,new_attributes)       ----step2
update table set attributes_column = attributes where primary_key = ?   ----step3 

但是这样的话,存在一个丢失更新的问题,两个线程ThreadA 和 ThreadB 同时运行到了step1得到相同的old_attributes,

然后同时做step2,最后ThreadA先做step3,而ThreadB后做step3,这样ThreadB就把ThreadA的属性更新给丢失了!

如何解决呢?基本两种思路,一种是悲观锁,另外一种是乐观锁; 简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。

1.1.1 悲观锁

  a)传统的悲观锁法(不推荐):

  以上面的例子来说明,在弹出修改工资的页面初始化时(这种情况下一般会去从数据库查询出来),在这个初始化查询中使用select ……for update nowait, 通过添加for update nowait语句,将这条记录锁住,避免其他用户更新,从而保证后续的更新是在正确的状态下更新的。然后在保持这个链接的状态下,在做更新提交。当然这个有个前提就是要保持链接,就是要对链接要占用较长时间,这个在现在web系统高并发高频率下显然是不现实的。

  b)现在的悲观锁法(推荐优先使用):

  在修改工资这个页面做提交时先查询下,当然这个查询必须也要加锁(select ……for update nowait),有人会说,在这里做个查询确认记录是否有改变不就行了吗,是的,是要做个确认,只是你不加for update就不能保证你在查询到更新提交这段时间里这条记录没有被其他会话更新过,所以这种方式也需要在查询时锁定记录,保证在这条记录没有变化的基础上再做更新,若有变化则提示告知用户。

1.1.2. 乐观锁

  a)旧值条件(前镜像)法:

  就是在sql更新时使用旧的状态值做条件,SQL大致如下 Update table set col1 = newcol1value, col2 = newcol2value…。 where col1 = oldcol1value and col2 = oldcol2value…。,在上面的例子中我们就可以把当前工资作为条件进行更新,如果这条记录已经被其他会话更新过,则本次更新了0行,这里我们应用系统一般会做个提示告知用户重新查询更新。这个取哪些旧值作为条件更新视具体系统实际情况而定。(这种方式有可能发生阻塞,如果应用其他地方使用悲观锁法长时间锁定了这条记录,则本次会话就需要等待,所以使用这种方式时最好统一使用乐观锁法。)

  b)使用版本列法(推荐优先使用):

  其实这种方式是一个特殊化的前镜像法,就是不需要使用多个旧值做条件,只需要在表上加一个版本列,这一列可以是NUMBER或 DATE/TIMESTAMP列,加这列的作用就是用来记录这条数据的版本(在表设计时一般我们都会给每个表增加一些NUMBER型和DATE型的冗余字段,以便扩展使用,这些冗余字段完全可以作为版本列用),在应用程序中我们每次操作对版本列做维护即可。在更新时我们把上次版本作为条件进行更新。在对一行进行更新的时候 限制条件=主键+版本号,同时对记录的版本号进行更新。 

  伪代码如下:

start transaction;
select attributes, old_version from table where