事务

事务是指满足ACID特性的一组操作,它们要么完全地执行,要么完全地不执行。

ACID特性

原子性(Atomicity)

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。

想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚。MySQL中的回滚日志(undo log)就是用于存放数据被修改前的值,如果出现异常,可以使用undo log来实现回滚操作。

一致性(Consistency)

数据库总是从一个一致性的状态转换到另外一个一致性状态。事务开始和结束之间的中间状态不会被其他事务看到。

隔离性(Isolation)

一个事务所作的修改在最终提交前,对其它事务是不可见的。

持久性(Durability)

一旦事务提交,则其所作的修改就会永久保存到数据库中。即使系统崩溃,修改的数据也不会丢失。

MySQL使用重做日志(redo log)实现事务的持久性。当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上。这样,在事务提交后,就算数据没来得及写回磁盘就宕机时,在下次重新启动后仍然能够成功恢复数据。

理解

事务的ACID特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:

  • 只要满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对数据库崩溃的情况。

隔离级别

在并发环境下需要关注事务的隔离性,SQL标准中定义了以下四种隔离级别。

未提交读(READ UNCOMMITTED)

事务中的修改即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。

提交读(READ COMMITTED)

一个事务从开始直到提交之前,所做的任何修改对其它事务都是不可见的。

这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。例如,T2读取一个数据,T1对该数据做了修改并提交,如果T2再次读取这个数据,那么读取的结果和第一次读取的结果不同。

可重复读(REPEATABLE READ)

可重复读保证了在同一事物中多次读取同样记录的结果是一致的。

该级别无法解决幻读问题,即当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。

可串行化(SERIALIZABLE)

该级别是最高的隔离级别,通过强制事务串行执行避免上面的幻读问题。

总结

隔离级别 脏读 不可重复读 幻读 加锁读
未提交读 ×
提交读 × ×
未提交读 × × ×
未提交读 × × ×

当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。所以,锁主要用于处理并发问题。

从数据库系统角度分为三种:排他锁、共享锁、更新锁。
从程序员角度分为两种:一种是悲观锁,一种乐观锁。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系数据库里用到了很多这种锁机制,比如按使用性质划分的读锁、写锁和按作用范围划分的行锁、表锁。

共享锁

共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

排他锁

排他锁(X锁)又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

表锁

每次操作锁住整张表,开销小,加锁快,锁粒度大,发生锁冲突的概率最高,并发度最低。

行锁

每次操作锁住一行数据,开销大,加锁慢,锁粒度小,发生锁冲突的概率最低,并发度最高。

数据库能够确定哪些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

CAS算法

CAS即compare and swap(比较并交换),是一种有名的无锁算法,在不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS算法涉及到三个操作数:

  • 要更新的变量V
  • 预期的值E
  • 新值N

仅当V值等于E值时,才会将V的值设置成N,否则什么都不做。最后CAS返回当前V的值。CAS算法需要你额外给出一个期望值,也就是你认为现在变量应该是什么样子,如果变量不是你想象的那样,就说明已经被别人修改过,就重新读取,再次尝试修改即可。

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时就会误以为它的值没有发生变化,这个问题称为ABA问题。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A,以此来防止不恰当的写入。

两种锁的适用场景

乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行重试,这样反倒是降低了性能,所以一般多写的场景下用悲观锁比较合适。

关系型数据库设计

函数依赖

部分函数依赖

设X、Y是关系R的两个属性集合,存在X→Y,若X’是X的真子集,存在X’→Y,则称Y部分函数依赖于X。

完全函数依赖

设X、Y是关系R的两个属性集合,X’是X的真子集,存在X→Y,但对每一个X’都有X’ !→Y,则称Y完全函数依赖于X。

传递函数依赖

设X、Y、Z是关系R中互不相同的属性集合,存在X→Y(Y !→X),Y→Z,则称Z传递函数依赖于X。

范式

第一范式(1NF)

在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。

所谓第一范式(1NF)是指数据库表的每一列(每个属性)都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。简而言之,第一范式就是无重复的列。

第二范式(2NF)

第二范式(2NF)要求实体的属性完全依赖于主关键字。

第三范式(3NF)

在满足第二范式的基础上,且不存在传递函数依赖,那么就是第三范式。简而言之,第三范式就是属性不依赖于其它非主属性。

ER图

ER图由三个部分组成:实体、属性、联系。

参考资料