## 数据库并发锁机制

---


在如今分布式、高并发、各种负载纵横天下的时代，支持高访问量成为检验一个系统合不合格的重要标准，然而我们除了在运算过程中要求系统更加效率外，在最终的数据存储过程中也希望其能够准确。

并发修改同一记录时为避免更新丢失，要么在应用层加锁，要么在缓存加锁，要么在数据库层使用乐观锁，使用 version 作为更新依据。


#### 悲观锁：

正如其名，它指对数据被外界（可能是本机的其他事务，也可能是来自其它服务器的事务处理）的修改持保守态度。在整个数据处理过程中，将数据处于锁定状态。悲观锁大多数情况下**依靠数据库的锁机制实现，以保证操作最大程度的独占性**。如果加锁的时间过长，其他用户长时间无法访问，影响程序的并发访问性，同时这样对数据库性能开销影响也很大，特别是长事务而言，这样的开销往往无法承受。

#### 乐观锁：

假设数据一般情况下不会造成冲突，只有在数据进行提交更新时，才会正式对数据的冲突与否进行检测，如果发现冲突了，则返回错误信息，让用户决定如何去做。fail-fast机制。

**小结：**

乐观锁和悲观锁之间选择的标准是冲突的频率、严重性。如果冲突较少或者冲突的后果不是很严重，通常情况下会选择乐观锁，容易实现且吞吐性高，能得到更好的并发性。如果冲突的结果对用户来说是非常严重的，可以使用悲观锁，适当牺牲一些性能。


**针对如何解决多线程并发产生的脏数据问题，本文简单列举一些常见案例及应对措施。**

#### 案例一：

本地起10个线程，分别执行10次，对数据库的一条记录的sum字段（初始值为0）+1操作，中间的业务逻辑我们忽略掉，如何保证执行完毕后sum的值为100？


表结构：

|字段名|	字段类型	|可空|	字段描述	| 使用备注|
| --- | --- | --- | --- |---|
|ID	|BIGINT(20)|	N	|主键ID|	无业务含义|
|SUM|	NUMBER(20)|N	|金额	|初始值为0|

**解决措施：**

利用数据库自身的事务来解决问题，update 表 set sum=sum+#increment#   where id=#id#，适用于一些只更新数量、金额的场景。
尽量不要采用在后台计算一个最终的sum值，然后通过 update 表 set sum=#sum#  where id=#id#，因为此时在读与写的时间间隔里，很有可能其它的线程已经读过或操作过

#### 案例二：

买家操作一笔订单，执行确认收货，假如同一笔订单打开了两个窗口，开始时在一个窗口确认成功，后来在另一个窗口又点了一次，此时应如何解决？

**解决措施：**

在执行“买家确认收货”操作时，我们通常会首先查出这笔订单，判断当前操作用户是否有执行权限，同时判断当前订单的状态是否是“等待买家确认收货”，。。。，如果满足这些前置条件，才允许后面的业务操作，更新数据库。
当然，存在另一种可能，如果是通过自动化脚本操作呢？两次操作几乎同时执行，也就是说，两次的前置校验都能顺利通过(因此那时，数据库记录还没来的及更新)，此时一个好的解决方案，操作时增加前置条件，比如确认收货的前置条件是“等待买家确认收货”，如果此时订单的状态变成了成功就无法操作。
update 订单表 set  status="交易成功"  where id=#orderId# and status="等待买家确认收货"
这样，第二次操作sq条件不满足，也就避免执行两次买家确认收货操作。

#### 案例三：

增加前置条件是一个不错的解决方案，但是，不是每个业务都有前置条件，或者说前置条件不明确、无规则，此时应如何解决？


|字段名|	字段类型	|可空|	字段描述	| 使用备注|
| --- | --- | --- | --- |---|
|ID|	BIGINT(20)|	N	|主键ID|	无业务含义|
|SUM|	NUMBER(20)|	N	|金额| 初始值为0|
|attribute_cc|	INT(11)	| N	|用于为attribute加锁	|
 

**解决措施：**

可以借助乐观锁，比较并交换（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

```

#### 注意：如果每次访问冲突概率小于 20%，推荐使用乐观锁，否则使用悲观锁。乐观锁的重试次数不得小于 3 次。

---
**参考资料：**

https://mp.weixin.qq.com/s?__biz=MjM5NzMyMjAwMA==&mid=2651477385&idx=1&sn=2db07fdbf148b47e88613cc648c9135a&scene=0&key=305bc10ec50ec19bec2be639b7fb5203e5461f172af5f913e52e8da9ab78acb42a407104893c37f972167b8d7b080be1&ascene=0&uin=Mzc2MDAyNDU%3D&devicetype=iMac+MacBookPro11%2C1+OSX+OSX+10.10.1+build(14B25)&version=11000003&pass_ticket=wALAc3GK4Gz2wCm4YcYmcfVkSIKhak3fraNYqyqYFaU%3D