Browse Source

乐观锁、悲观锁

wenwan 8 years ago
parent
commit
95c59d36df
1 changed files with 66 additions and 4 deletions
  1. 66 4
      data-base/锁机制.md

+ 66 - 4
data-base/锁机制.md

@@ -5,8 +5,21 @@
 
 在如今分布式、高并发、各种负载纵横天下的时代,支持高访问量成为检验一个系统合不合格的重要标准,然而我们除了在运算过程中要求系统更加效率外,在最终的数据存储过程中也希望其能够准确。
 
-**针对如何解决多线程并发产生的脏数据问题,本文简单列举一些常见案例及应对措施。
-**
+####悲观锁:
+
+正如其名,它指对数据被外界(可能是本机的其他事务,也可能是来自其它服务器的事务处理)的修改持保守态度。在整个数据处理过程中,将数据处于锁定状态。悲观锁大多数情况下**依靠数据库的锁机制实现,以保证操作最大程度的独占性**。如果加锁的时间过长,其他用户长时间无法访问,影响程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受。
+
+####乐观锁:
+
+假设数据一般情况下不会造成冲突,只有在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回错误信息,让用户决定如何去做。fail-fast机制。
+
+**小结:**
+
+乐观锁和悲观锁之间选择的标准是冲突的频率、严重性。如果冲突较少或者冲突的后果不是很严重,通常情况下会选择乐观锁,容易实现且吞吐性高,能得到更好的并发性。如果冲突的结果对用户来说是非常严重的,可以使用悲观锁,适当牺牲一些性能。
+
+
+**针对如何解决多线程并发产生的脏数据问题,本文简单列举一些常见案例及应对措施。**
+
 #### 案例一:
 
 本地起10个线程,分别执行10次,对数据库的一条记录的sum字段(初始值为0)+1操作,中间的业务逻辑我们忽略掉,如何保证执行完毕后sum的值为100?
@@ -21,7 +34,7 @@
 
 **解决措施:**
 
-乐观锁机制,利用数据库自身的事务来解决问题,update 表 set sum=sum+#increment#   where id=#id#,适用于一些只更新数量、金额的场景。
+利用数据库自身的事务来解决问题,update 表 set sum=sum+#increment#   where id=#id#,适用于一些只更新数量、金额的场景。
 尽量不要采用在后台计算一个最终的sum值,然后通过 update 表 set sum=#sum#  where id=#id#,因为此时在读与写的时间间隔里,很有可能其它的线程已经读过或操作过
 
 #### 案例二:
@@ -46,9 +59,58 @@ update 订单表 set  status="交易成功"  where id=#orderId# and status="等
 |SUM|	NUMBER(20)|	N	|金额| 初始值为0|
 |attribute_cc|	INT(11)	| N	|用于为attribute加锁	|
  
+
 **解决措施:**
 
-可以借助memcache用到的一种同步机制(CAS),比较并交换,在数据库表增加一个冗余字段,每次操作都会自动+1。执行业务时,首先会从数据库读取该字段信息,更新业务数据时,会自动比较attribute_cc的值是否有变化,如果有变化,表示刚才读的信息已变化过,需要重新操作。
+可以借助乐观锁,比较并交换(CAS),在数据库表增加一个冗余字段,每次操作都会自动+1。执行业务时,首先会从数据库读取该字段信息,更新业务数据时,会自动比较attribute_cc的值是否有变化,如果有变化,表示刚才读的信息已变化过,需要重新操作。
+
+**特别注意:**
+
+attribute_cc是针对整条记录设置的行锁,如果数据库表有很多类似于features这样的json复合字段,我们将锁的粒度范围进一步缩小,每一个features配一个features_cc,features_cc的作用就是features的乐观锁版本的控制,可以很好规避使用attribute_cc与整个字段冲突的尴尬。
+
+
+#### 案例四:
+
+商品表items表中有一个字段status,status=1表示商品未被下单,status=2 表示该商品已经被下单,那么我们对每个商品下单前必须保证此商品的status=1。假设有一件商品,其id 为1000。
+
+常规思路:
+
+* 先查询商品状态  select status from items where id=1000
+* 生成订单 
+* 修改商品状态 update items set status=2 wehre id=1000
+
+在高并发环境下,在操作第三步update时,很有可能其它人已经先一步把商品的status修改为2
+
+悲观锁思路:从查询出items信息时就把当前的数据锁定,直到我们修改完毕后再解锁。使用悲观锁,需要关闭mysql数据库的自动提交属性,因为mysql默认使用autocommit模式,当你执行一个更新操作后,mysql会立刻将结果提交。
+
+步骤:
+
+* set autocommit=0
+* 开始事务。begin、begin work、start transaction(三者选一就可以)
+* 查询出商品信息
+
+  select status from items where id=1000 for update;
+* 生成订单 insert into orders(id,item_id) values(null,1000)
+* 修改商品状态 update items set status=2 wehre id=1000
+* 提交事务  commit 
+
+我们使用select ... for update的方式,通过数据库实现了悲观锁。id=1000那条记录被我们锁定了,其它事务必须等本次事务提交后才能执行。这样我们就可以保证当前的数据不会被其它事务修改。
+
+注:用select ... for update 同一条记录时会等待其它事务结束后才执行,一般select...不受影响。比如当我们执行select status from items where id=1000 for update后,另外的事务也执行了select status from items where id=1000 for update 则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但如果第二个事务中执行select status from items where id=1000,则能正常查询数据,不受第一个事务的影响。
+
+mysql innoDB默认使用行锁,需要明确指定主键,否则mysql将会执行表锁(将整个表锁住)。除了主键外,使用索引也会影响数据库的锁定级别。
+
+#### 案例五:
+
+商品减库存时,如果在秒杀等高并发的场景下,如果采用version作为乐观锁,虽然每次只有一个事务能更新成功,但业务感知上会有大量的操作失败。解决方案可以采用库存数做为乐观锁
+
+```
+update item
+set quantity=quantity - #sub_quantity#
+where item_id=#id# 
+	  and quantity - #sub_quantity# > 0
+
+```
 
 
 **参考资料:**