数据库事务和锁

数据库的事务指的是一组业务处理逻辑,这组逻辑处理要求要么都成功要么都失败(原子性),执行前数据和执行后数据状态总是一致的(一致性),事务之间的处理不会互相影响(隔离性),事务执行完毕之后数据必须持久化了()。一个具体的数据库产品会用一些技术手段来实现事务的这些特性。比如用undo log实现数据回滚,不同的数据库锁策略来实现不同的隔离性。清晰地了解这些特性的实现细节有助于正确地设计和实现复杂的业务逻辑。接下来主要介绍下数据库事务隔离性是怎么实现的。

 

事务的隔离性本质上是如何控制对数据库数据的并发读写。数据库的并发读写会带来如下几个问题:

  • 脏读
  • 不可重复读
  • 幻读
  • 由于事务回滚带来的丢失更新(第一类丢失更新)
  • 由于事务写互相覆盖带来的丢失更新(第二类丢失更新)

因此对并发访问的控制实际上转换成解决上面几个并发问题。对于解决不同的并发问题需要不同程度的使用锁,事务隔离性分为

  • 读未提交(啥都不管,事务内可能读到其他事务还没commit前的数据)
  • 读提交(RC):解决了脏读
  • 可重复读(RR):解决了脏读和不可重复读)
  • 可串行化(Serializable):解决脏读,不可重复度以及幻读

( 下面介绍的内容以mysql为准)

数据库锁根据锁定互斥行为的不同分为:

  • 共享锁(读锁或S锁):若事务T对某一对象加上了共享锁,那么任何事务都可以对数据加共享锁读数据,但是不能加X锁或修改数据
  • 排它锁(写锁或X锁):若事务T给对象施加排他锁,只有T事务能够修改数据,其他任何事务不允许加S或X锁,直到T释放A上的锁

根据锁定对象不同主要有:

  • 表锁:对整张表进行锁定,一般用在DDL操作时候
  • 索引记录锁:对单条数据记录进行锁定
  • 间隙锁:对某一数据区间进行锁定
  • next-key-lock:记录锁+间隙锁

按照最朴素的做法,要实现RC和RR的隔离效果所有的读数据必须加共享锁,写数据加排他记录锁,而要防止幻读甚至要进行表级锁定,串行化操作,这个效率是无法接受的,因此为了提高并发效率,很多数据库软件都实现了MVCC(多版本并发控制)的方式,基本的实现原理如下:

每条数据记录增加三个隐藏字段,其中有创建版本号,删除版本号,回滚指针

DB系统每次开启一个新事务就会给事务生成一个递增的唯一的事务ID,并且生成一个read view,包含了当前系统的活跃事务ID列表,以RR隔离级别下,MVCC行为

1. 插入操作将记录的创建版本号赋值为当前版本

2. 删除操作将记录的删除版本号赋值为当前事务ID

3  更新操作将当前记录标识成删除,删除版本号赋值为当前事务ID,并且新创建一条记录,新纪录的创建版本号赋值成当前事务ID

4.select操作需要避免脏读,不可重复度,因此读取的数据需要包括在事务开启之前没有改变过,或者事务开启之后才被删除的,delete_version==null && create_version<up_limit_tid 或者 delete_version>up_limit_tid

 

RC中的MVCC行为只是read view在每次执行读之前都会更新,因此会读取到最新的commit的事务做出的修改。

 MVCC实现了并发地无阻塞读,这个无阻塞读实际上是快照读,不加锁,

     如简单的 

  • select * from xxx where ?

相对快照读还有当前读,当前读是需要加锁的:

  •  select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (…);
  • update table set ? where ?;
  • delete from table where ?;

 

在MVCC的支持下各个隔离级别加锁情况如下:

Read Committed (RC)

快照读不加锁

针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

如果一个更新,或删除语句没有走索引,RC会对全表加锁,然后对不满足条件的记录再进行解锁,这实际上mysql为了效率违背了两阶段锁协议的规范,

 

Repeatable Read (RR)

快照读不加锁

 

针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

 

 

丢失更新的解决方式:

 丢失更新发生的根本原因是基于相同的版本的数据的写操作都允许成功,乐观锁能够避免第一类和第二类丢失更新

1.悲观锁的方式: slect * from table for update;

2.乐观锁版本号的方式

 

拍卖相关

事务的隔离级别的选择

有可重复读的需求,无防止幻读的需求,需要防止丢失更新;

1.乐观锁解决丢失更新问题

2.定制化的更新接口解决可重复读需求,这种方法一定情况下也可以解决幻读问题

通过这些办法使得隔离级别在RC下也能满足需求,相比RR没有间隙锁,能提高系统的并发能力

 

保证金临界状态

根本的解决办法只有使用悲观锁

 

如果有高并发的场景价格的乐观锁可以写成类似库存的方案

在更新拍品记录的语句中将乐观锁版本号的比较改成 bid_price>current_price and status>'bidding',这样在出价价格比较分散的情况下能够一定程度提高系统并发能力,在用版本号做为乐观锁的情况下,用户A出价1000,用户B出价2000,当用户A和用户B冲突并且A抢先更新了乐观锁,B的出价就会失败,哪怕B出价更高。 

 

拍卖系统数据库高可用方案

数据库高可用方案:

  • 一写多读:单master(负责写),多slave(负责读)当做了一写多读后,因为从多个读库中均衡读取数据,如果某一台读库宕机,那么隔离屏蔽,这样就做到了读的高可用。但写库还是一个单点,并且读库和写库之间数据同步有一定延迟。这种方案一旦master挂掉了拍卖的竞拍业务也就挂了,导购相关的读业务还能继续提供服务;
  • 主库冷备+多读:正常工作的时候同一写多读一样,当主库发生故障,写切换到备份机上,这时候对于新数据的写可以继续,对于历史数据的读也可以继续,对于历史数据的更新可能无法进行,当主库故障,业务很大程度上能够继续,不至于完全挂掉,这种部署方式对于一口价这样的业务基本是可行的,但是拍卖的出价是一个强一致上下文相关的过程,凡是读写分离的方式主备之间可能存在数据不一致的方案都不能采用
  • 双master:同时存在两个master提供写,master之间采用数据强同步,这样能够做到一个写节点挂了另一个节点就能顶上去。缺点是两个master之间的数据同步超时或者抖动当次业务处理就可能判定为失败,因此双master的重点是两个master所在的机房距离尽可能近,阿里机房距离在50km以内的差不多延迟在1-2ms,如果800km就可能达到几十ms,一次出价操作涉及到5次左右的数据写,如果距离近差不多增加10-20ms左右的延迟,问题不是很大;
  • 三master:在数据同步上当前master向两个stand by的master做数据强同步,只要有其中一个成功了就可以返回,在机房之间网络出现波动的时候可以提高一定的可用性,数据延迟也不会增加

猜你喜欢

转载自rrsongzi-gmail-com.iteye.com/blog/2288995