数据库锁机制

2019-12-07 04:09栏目:网络技术
TAG:

数据库锁机制,锁机制

一、为什么需要了解锁

1.1 死锁问题

1.2 并发问题导致的不正确数据的读取和存储,破坏数据一致性的

  • 丢失更新:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题
  • 脏读:一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。
  • 不可重复读:一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
  • 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

二、锁的分类

2.1 数据库维度

  • 共享锁:用于不更改或不更新数据的操作(只读操作)。共享锁允许并发事务读取同一个资源,数据资源上存在共享锁时,任何其他事务不允许修改数据
  • 排它锁: 用于数据修改,确保不会同时多重更新同一数据。资源上存在排他锁时,其他任何事务不允许给资源上锁,当资源上有其他锁时,也无法对其加上排它锁

PS:只有共享锁与共享锁相互兼容,共享锁与排它锁、排它锁之间都互不兼容

  • 更新锁 

2.2 数据库锁机制

DBMS SELECT UPDATE INSERT DELETE
MySQL(InnoDB) 不加锁 排它锁 排它锁 排它锁
SQL SERVER 共享锁 更新锁  排它锁 排它锁

 

 

 

 

 这两种锁机制的区别在于MySQL的查询与更新操作互相不阻塞;而SQL SERVER的更新锁转化成排它锁之前,其查询与更新操作互相不阻塞,当更新锁要转化为排它锁时,需要等待共享锁的释放,当更新锁转化为排它锁后,查询数据需要等待排它锁的释放。

参考:
[数据库锁机制]()
[InnoDB锁机制]()
[SQL SERVER锁机制]()

2.3 程序员思想维度

  • 悲观锁
  •  乐观锁
update table 
set date=1,version=version+1
where id=#{id} and version=#{version};

参考:
[乐观锁与悲观锁]()

2.4 乐观锁另一种实现方式CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

public class AtomicInteger extends Number implements java.io.Serializable {
  private volatile int value; 

  public final int get() { 
    return value; 
  } 

  public final int getAndIncrement() { 
    for (;;) { 
      int current = get(); 
      int next = current + 1; 
      if (compareAndSet(current, next)) 
        return current; 
    } 
  } 

  public final boolean compareAndSet(int expect, int update) { 
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
  } 
}

 

参考:
[乐观锁的一种实现方式—CAS]()

三、案例分析

3.1 初审统计数据迁移

  • 迁移背景:原有的统计方式采用的是实时count的方法获取统计数据,造成的问题是查询慢且无法获取长时间段的统计数据(sql超时)、无法获取某日统计数据的快照(前一天的待审核数据会变成今天的审核通过数据)
  • 采用迁移方式:使用raptor迁移平台,扫描审核记录表,取出累计统计数据后进行加1操作,然后更新到统计表中。由于平台特性,数据迁移过程具有高并发性,由于强行采用先读取后更新的方式,会造成丢失更新的情况,于是这里考虑采用CAS

step1:

 

select id,passCount,rejectCount,hideCount,warnCount,waitCount 
from book.TradeItemAuditCount 
where type = #{type} and date = #{date} and editor = #{editor} 
and isDeleted = 0 limit 1

step 2:【失败重试】

update book.TradeItemAuditCount 
set passCount = #{passCount} , rejectCount = #{rejectCount} , hideCount = #{hideCount} , warnCount = #{warnCount} , waitCount = #{waitCount} , updated = #{updated}
where id = #{id} and passCount = #{oldPassCount} and rejectCount = #{oldRejectCount} and hideCount = #{oldHideCount} and warnCount = #{oldWarnCount} and waitCount = #{oldWaitCount}
and isDeleted = 0 limit 1
  •  处理结果 :一共扫描审核结果4335668条数据,对重试次数超过100的更新操作进行记录,发现更新操作出现大部分的重试,任务本身DB写操作的qps较低【都不需要通过控制台限制速率..】

3.2 商品库存

  • 商品库存与上述案例1一致,都是对数据记录进行加减操作,发现库存的更新方式如下:
update 库存表
set stock=stock-1
where id=#{id}
  • 直接使用数据库的排它锁就简单的避免了并发导致的丢失更新问题,之前提到的一次只有一个事务拥有资源的排它锁,并发的更新操作都试图占有资源的排它锁,当资源上存在排它锁时,其他更新操作需要等待锁的释放
  • 相比案例1的解决方案,案例2的解决方式直接使用了MySQL InnoDB更新操作本身就拥有的排它锁,不需要额外的开销,而案例1不必要的查询操作以及多次的重试操作严重影响到了数据迁移的性能,所以案例1是反面例子..

3.3 商品打标

  • 随着上打标的qps上涨,出现达标更新数据丢失的情况
{"tags":"16,32,233,22","itemState":1,"hd":"ai:4|nd:18","au":"baoming"}
  • 处理方案

  1.乐观锁:采用CAS

update TradeItem
set extra=#{extra}
where tradeItemId=#{tradeItemId} and extra=#{oldExtra}

这里使用长字符串做更新条件,会影响到SQL性能

  2.乐观锁:采用数据版本 表中新增version字段标识数据版本,作为数据更新的检查方式

update TradeItem
set extra=#{extra} , version=version+1
where tradeItemId=#{tradeItemId} and version=#{version}

此方案改造较大,还需要为表新增字段,而且采用乐观锁拥有这一律的弊端:重试带来的时间代价,一旦并发量上涨,某次更新操作的重试次数也会随之上涨,直接影响到暴露服务的响应时间。【限制重试次数能够一定程度上控制更新操作的响应时间,但是仍然会出现更新丢失的现象(让调用方进行重试操作,分摊单次请求的响应时间?)】  

  3. 悲观锁:更新丢失的根本原因是执行查询、修改两个操作之间数据被另一事务修改了,单纯的UPDATE操作其实也是进行着先查询后修改的操作,没有产生更新丢失是因为数据上存在排它锁(sql server则是更新锁),在执行期间并不允许其他修改。同理我们将要打标的商品记录加上排它锁或者更新锁就能解决问题。 

MySQL:

start transaction;
SELECT extra
FROM TradeItem 
WHERE tradeItemId=#{tradeItemId}
FOR UPDATE;
UPDATE TradeItem 
SET extra = bdo.AddTag(tag,extra)
WHERE tradeItemId=#{tradeItemId};
commit;

SQL SERVER:

BEGIN TRANSACTION --开始一个事务
SELECT extra
FROM TradeItem WITH (UPDLOCK)
WHERE tradeItemId=#{tradeItemId}
UPDATE TradeItem 
SET extra = bdo.AddTag(tag,extra)
WHERE tradeItemId=#{tradeItemId}
COMMIT TRANSACTION --提交事务

该方案避免了重试带来的开销,同时使用排它锁(更新锁)也没有额外增加锁的开销

四、悲观锁乐观锁的取舍

一、 为什么需要了解锁 1.1 死锁问题 1.2 并发问题导致的不正确数据的读取和存储,破坏数据一致性的 丢失更新:当...

版权声明:本文由澳门新葡亰平台游戏发布于网络技术,转载请注明出处:数据库锁机制