丢失更新是数据中一个比较常见的经典问题,在做项目时我们有时可能会没有注意到这个问题,但这个问题相当重要,有时会带来比较严重的结果。下面我们就来讨论下这个丢失更新。
一、什么是丢失更新:
用一个操作过程来说明:
(1)会话Session1 中的一个事务获取(查询)一行数据,并显示给一个用户User1。
(2)会话Session2 中的另一个事务也获取这一行,但是将数据显示给另一个用户User2。
(3)User1 使用应用修改了这一行,让应用更新数据库并提交。会话Session1 的事务执行完毕。
(4)User2 也修改这一行,让应用更新数据库并提交。会话Session2 的事务执行完毕。
这个过程就叫做“丢失更新”,因为第(3)步做的操作会全部丢失(被第4步操作覆盖),最终数据库只会保存第(4)步的更新结果。这个情况在有些系统中可能不会有影响,但在有些系统中可能就影响很大了,举个简单的例子:
财务系统加工资,若公司本次调薪决定给员工张三加1k人民币,财务部两名操作人员A和B,过程情况若是这样的:
1)A操作员在应用系统的页面上查询出张三的薪水信息,然后选择薪水记录进行修改,打开修改页面但A突然有事离开了,页面放在那没有做任何的提交。
2)这时候B操作员同样在应用中查询出张三的薪水信息,然后选择薪水记录进行修改,录入增加薪水额1000,然后提交了。
3)这时候A操作员回来了,在自己之前打开的薪水修改页面上也录入了增加薪水额1000,然后提交了。
其实上面例子操作员A和B只要一前一后做提交,悲剧就出来了。后台修改薪水的sql:update 工资表 set salary = salary + 增加薪水额 where staff_id = ‘员工ID’。这个过程走下来后结果是:张三开心了这次涨了2k,操作员A和B都郁闷了。
二、解决思路:
基本两种思路,一种是悲观锁,另外一种是乐观锁; 简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。
三、解决方案1(悲观锁)
a)传统的悲观锁法(不推荐):
以上面的例子来说明,在弹出修改工资的页面初始化时(这种情况下一般会去从数据库查询出来),在这个初始化查询中使用select ...for update nowait, 通过添加for update nowait语句,将这条记录锁住,避免其他用户更新,从而保证后续的更新是在正确的状态下更新的。然后在保持这个链接的状态下,在做更新提交。当然这个有个前提就是要保持链接,就是要对链接要占用较长时间,这个在现在web系统高并发高频率下显然是不现实的。
b)现在的悲观锁法(推荐优先使用):
在修改工资这个页面做提交时先查询下,当然这个查询必须也要加锁(select ...for update nowait),有人会说,在这里做个查询确认记录是否有改变不就行了吗,是的,是要做个确认,只是你不加for update就不能保证你在查询到更新提交这段时间里这条记录没有被其他会话更新过,所以这种方式也需要在查询时锁定记录,保证在这条记录没有变化的基础上再做更新,若有变化则提示告知用户。
四、解决方案2(乐观锁)
a)旧值条件(前镜像)法:
就是在sql更新时使用旧的状态值做条件,SQL大致如下 Update table set col1 = newcol1value, col2 = newcol2value…. where col1 = oldcol1value and col2 = oldcol2value….,在上面的例子中我们就可以把当前工资作为条件进行更新,如果这条记录已经被其他会话更新过,则本次更新了0行,这里我们应用系统一般会做个提示告知用户重新查询更新。这个取哪些旧值作为条件更新视具体系统实际情况而定。(这种方式有可能发生阻塞,如果应用其他地方使用悲观锁法长时间锁定了这条记录,则本次会话就需要等待,所以使用这种方式时最好统一使用乐观锁法。)
b)使用版本列法(推荐优先使用):
其实这种方式是一个特殊化的前镜像法,就是不需要使用多个旧值做条件,只需要在表上加一个版本列,这一列可以是NUMBER或 DATE/TIMESTAMP列,加这列的作用就是用来记录这条数据的版本(在表设计时一般我们都会给每个表增加一些NUMBER型和DATE型的冗余字段,以便扩展使用,这些冗余字段完全可以作为版本列用),在应用程序中我们每次操作对版本列做维护即可。在更新时我们把上次版本作为条件进行更新。
c)使用校验和法(不推荐)
d)使用ORA_ROWSCN法(不推荐)
五、结论:
综上所述,我们对丢失更新问题建议采取上面的悲观锁b方法或乐观锁b方法(蓝色字体已标注),其实这两种方式的本质都一样,都是在更新提交时做一次查询确认在更新提交,我个人觉得都是乐观的做法,区别在于悲观锁b方法是通过select..for update方式,这个可能会导致其他会话的阻塞,而乐观锁b方法需要多一个版本列的维护。
个人建议:在用户并发数比较少且冲突比较严重的应用系统中选择悲观锁b方法,其他情况首先乐观锁版本列法。