事务 & 锁

2021-04-12  本文已影响0人  灏玮

事务的概念

事务的概念来自于两个独立的需求:并发数据库访问,系统错误恢复。
一个事务是可以被看作一个单元的一系列SQL语句的集合。

事务的特性(ACID)

事务的隔离级别

如果不对数据库进行并发控制,可能会产生异常情况:

  1. 脏读(Dirty Read)
    当一个事务读取另一个事务尚未提交的修改时,产生脏读。
    同一事务内不是脏读。一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚,也就是说读取除的数据其实是错误的。
  2. 非重复读(Nonrepeatable Read)
    一个事务对同一行数据重复读取两次,但是却得到了不同的结果。同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。
  3. 幻象读(Phantom Read,幻读)
    事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
    当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会产生幻读的问题。
  4. 丢失修改(Lost Update)
    第一类:当两个事务更新相同的数据源,如果第一个事务被提交,第二个被撤销,那么连同第一个事务做的更新也被撤销。
    第二类:有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失败。

为了兼顾并发效率和异常控制,在标准SQL规范中,定义了4个事务隔离级别,(Oracle 和 SQL Server 对标准隔离级别有不同的实现)

  1. 未提交读(Read Uncommitted)
    直译就是“读为提交”,意思是即使一个更新语句没有提交,但是别的事务可以读到这个改变。允许脏读。
  2. 已提交读(Read Committed)
    直译就是“读提交”,意思是语句提交以后,即执行了 Commit 以后别的事务就能读到这个改变,只能读取到已经提交的数据。 Oracle等多数数据库默认都是该级别。
    不允许脏读,但会出现非重复读。
  3. 可重复度(Repeatable Read)
    是说在同一个事务里面先后执行同一个查询语句的时候,得到的结果是一样的。
    不允许脏读,不允许非重复度,但是会出现幻读。
  4. 串行读(Serializable)
    序列化,是说这个事务执行的时候不允许别的事务并发执行。完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
    不允许不一致现象的出现。

事务隔离的实现——锁

  1. 共享锁(S锁)
    用于只读操作(SELECT),锁定共享的资源。共享锁不会阻止其他用户读,但是阻止其他的用户写和修改。
  2. 更新锁(U锁)
    用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。
  3. 独占所(X锁,也叫排他锁)
    一次只能有一个独占锁用在一个资源上,并且阻止其他所有的锁包括共享锁。写是独占锁,可以有效的防止“脏读”。

Read Uncommitted 如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

Read Committed 读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。可以通过“瞬间共享读锁”和“排他写锁”实现。

Repeatable Read 读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。可以通过“共享读锁”和“排他写锁”实现。

Serializable 读加共享锁,写加排他锁,读写互斥。

乐观锁 悲观锁

基本概念

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

实现方式

明确:乐观锁和悲观锁是两种思想,它们的使用是非常广泛的,不局限于某种编程语言或数据库。

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁,也可以是对数据加锁(如MySQL中的排他锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制。

CAS(Compare And Swap)

CAS操作包括了3个操作:

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
这里引出一个新问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

版本号机制

除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1.当某个线程查询数据时,将该数据的版本号一起查出来;当该新城更新数据时,判断当前版本号与之前的版本号是否一直,如果一直才进行操作。
需要主义的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够编辑数据版本的字段,如时间戳等。

优缺点和适用场景

乐观锁和悲观锁并没有优劣之分,他们有格子适合的场景。

  1. 功能限制
    与悲观锁相比,乐观锁适用的场景受到了更多的限制,无路是CAS还是版本号机制。
    例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
  2. 竞争激烈程度
    如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

面试官追问:乐观锁加锁吗?

  1. 乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了。
  2. 有时乐观锁可能与加锁操作合作,例如,MySQL在执行update时会加排他锁,但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。

面试官追问:CAS有哪些缺点?

CAS这种实现方式有什么缺点?

  1. ABA问题:
    假设有两个线程——线程1和线程2,两个线程按照顺序进行一下操作:
    (1) 线程1读取内存中数据为A
    (2) 线程2将该数据修改为B
    (3) 线程2将该数据修改为A
    (4) 线程1对数据进行CAS操作
    在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。
    ABA似乎没有什么危害。但在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
    对于ABA问题,比较有效的方案是引入版本号,内存中的值发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
  2. 高竞争下的开销问题
    在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阀值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。
  3. 功能限制
    CAS的功能比较受限,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
    (1) 原子性不一定能保证线程安全
    (2) 当涉及到多个变量(内存值)时,CAS也无能为力
    除此之外,CAS的实现需要硬件层面处理器的支持,灵活性受到限制。
上一篇下一篇

猜你喜欢

热点阅读