什么是离线并发
- 用户A将数据R(C1,C2)读取到A的浏览器中。
- 用户B将数据R(C1,C2)读取到B的浏览器中。
- 用户A在浏览器上将数据修改为R(C1’,C2),同时更新到数据库。
- 用户B在浏览器上将数据修改为R(C1,C2’),同时更新到数据库。
上述过程存在两个问题,第一,第4步B在修改数据的时候数据库中的数据和B的浏览器中数据已经不一致了;第二,如果程序按照哪个字段变化在数据库中更新哪 个字段的方式处理的话,那么经过上述四步修改,数据库中R行的内容是(C1’,C2’),这和A或者B的想法都不同(A认为是(C1’,C2),B认为是 (C1,C2’))。上述过程中A对数据库的修改过程或者B对数据库的修改过程,都是无法根据数据库的最新内容做修改,所以称为为离线。A和B同时对记录R进行修改叫离线。以上的环境叫离线并发。
锁机制
锁机制,就是在需要修改的数据上加互斥锁,通过互斥锁避免数据被同时修改。锁机制更具其应用环境又分为乐观锁和悲观锁.
乐观离线锁
之所以叫离线锁是因为这种锁不是长时间的锁,而且一个业务事务中可能同时包含了几个系统事务。而乐观是相对悲观而言的,表示预计冲突不总是发生,以其得到最大的性能。可能对业务事务,系统事务的概念大家有些陌生。按我的理解,可以这样解释:首先,你要明白事务不仅仅是一个技术问题(系统事务),更是一个领域问题(业务事务),举例说明,我们编辑一篇文章,显示编辑页面的动作可能涉及一个系统事务,提交表单的时候又涉及了一个系统事务,而整体可以看成一个业务事务。
乐观锁,指认为冲突很少发生,所以只是在数据修改的时候比较修改的基础数据和数据库中的数据是否相同,相同则修改,否则提示用户重新装入数据库中已经变化的数据。
一个帐单系统中增加帐单并计算销售税。某个会话增加一个帐单,然后查找顾客的地址来计算税率。但在生成帐单的同时,一个进行顾客信息维护的会话编辑了顾客的地址信息。由于税率和住址有关,生成帐单是使用的税率就不正确了,但由于帐单生成会话不会修改地址信息,因而就不会检测到冲突。从这个问题中,我们能深刻体会到事务不仅仅是一个技术问题,更是一个领域问题。
乐观离线锁通过检查在会话读取一条数据以后没有其他的会话修改该数据来保证数据的一致性.可以在任何时候获取一个乐观离线锁,但它只在获取该锁地系统事务过程中有效.因此,业务事务为了不破坏记录数据,必须对它在每个系统事务中的变更集成员申请乐观离线锁.也就是说,只要系统事务中有对数据库的修改,就需要获取乐观离线锁.
最常见的实现方式是是为每条记录关联一个版本号,当某条记录被装载是,该版本号与其他会话状态一样,由会话本身来维护。获取乐观离线锁的本质就是将会话数据中的版本号与当前记录数据的版本号相比较。一旦验证成功,所有变化(包括版本号的增加)就可以提交。防止不一致的记录数据是通过版本号来完成的,因为一个拿着旧版本号的会话无法获得乐观离线锁。
在基于关系数据库的系统中,锁地检查是通过在所有更新或删除记录的SQL语句中加上对版本号的判别来完成的。用一条SQL就可以同时获取锁和更新数据。最后一步是检查业务事务执行的SQL所返回的行数。行数为1代表成功,0代表记录被更改过或者已经被删除。返回行数0是,要将系统事务回滚以防止不一致的数据进入数据库。这样一来,业务事务必须要么被取消,要么解决冲突并重试。
通常实现乐观离线锁是通过在UPDATE和DELETE语句中加上版本号检查来实现的,但这样不能防止不一致读。例如在一个账单系统中增加账单并计算消费税。某个会话增加一张账单,然后查找客户的地址来计算税率。但账单生成的同时,一个进行客户信息维护的会话编辑了顾客的地址信息。由于税率和住址有关,生成账单时使用的税率就不正确了,由于账单生成会话不会修改地址信息,因而就不会检测到冲突。
与其他锁模式一样,tongchang对于企业应用中某些棘手的并发和时序问题,乐观离线锁本身并不能提供充分的解决方案。必须再次强调,在企业应用中的并发管理更是一个领域问题,而不只是技术问题。上面所说的顾客信息计算税率也可能允许的,但究竟应该使用那个版本呢?这就是一个业务问题。或者考虑一个集合。当两个会话同时向集合中加入元素时会发生什么呢?典型的乐观离线锁模式无法防止这种情况的发生。
乐观离线锁仅仅在业务事物提交时最后的系统事物中报告冲突。但通常提前冲突会更有效。可以提供checkCurrent()方法随时检查是否有其他人改动了数据。虽然不能保证不冲突,但提前通知冲突来停止一个复杂的过程也是值得的。随时使用checkCurrent()检测冲突以提前终止事物是有用的,但要记住,它并不保证提交时不会失败。
悲观离线锁
就锁类型而言,第一个选择是独占写锁,只在业务事物获取锁是为了编辑会话数据是才需使用该锁。它通过避免两个业务事务同时编辑一份数据来消除冲突。这种锁模式忽略了对数据的读,因此如果对数据读出的要求不是很高是,应该使用这种方式。
通常在数据库中建立一张lock表,该表的字段包括,表明,唯一索引,时间,用户信息等。在用户读取数据准备修改的时候,首先判断lock表中是否存在自己将要读取的数据。如果不存在,则在lock表中添加一条记录,记录对那张表的哪行数据进行修改;如果存在,在判断时间字段是否超时。如果超时,则更新lock表中本条记录的时间字段。(防止死锁的必要手段)。如果存在,也不超时,说明本条记录正在被其他用户修改,则返回并发信息。
粗粒度锁
粗粒度锁是覆盖多个对象的单个锁,这样不但简化了加锁行为本身,而且让你不必为了给他们加锁而加载所有的对象。实现粗粒度锁的第一步是为一组对象建立一个控制点。这使得只需要一个锁就可以锁住一堆对象,然后尽量提供最直接的方法找到他们的锁,从而减少在获取锁时为了标识该组而读取的对象数目。
用乐观离线锁让组中的对象共享同一个版本号来建立一个控制点,这意味这它们共享同一个版本号,而不是说他们的版本相同。增加这额版本号是,就成为一个锁住组中的所有对象的共享锁。
参考
锁机制(离线并发,乐观锁,悲观锁)
乐观离线锁