安装
Mac
Docker
设计规范
性能优化
事务
四大特性 ACID
- 原子性:事务是最小的执行单位,不可分割
- 一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的
- 隔离性:并发访问时,事务不会彼此干扰,各事务之间相互独立
- 持久性:一个事务被提交之后,即使数据库运行发生故障也不应该对其有任何影响
隔离级别
隔离级别越高,效率就越低
读未提交 Read Uncommitted
- 描述:一个事务还没提交时,它做的变更就能被别的事务看到;
- 实现:直接返回记录上的最新值;
- 存在问题:脏读、丢失修改、不可重复读、幻读
读提交 Read Committed
- 描述:一个事务提交之后,变更才会被其他事务看到
- 实现:数据库会创建视图,访问的时候以视图的逻辑结果为准,这个视图在每个 SQL 语句开始执行时创建
- 存在问题:丢失修改、不可重复读、幻读
可重复读 Repeatable Read
- 描述:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
- 实现:在事务启动时创建视图,访问时以视图逻辑结果为准
- 存在问题:幻读(InnoDB 已解决)
串行化 Serializable
- 描述:对于同一行记录,当出现读写锁冲突时,后访问的事务必须等前一个事务执行完成,才能继续执行。
- 实现:直接用加锁的方式,避免并行访问;
- 存在问题:效率极低
并发事务引起的常见问题
脏读
当一个事务正在访问数据且对数据进行了修改,但修改还没有提交到数据库中。另外一个事务特访问并使用了这个数据,那么这个数据对另一个事务来说就是脏数据。
修改丢失
一个事务读取数据时,另一个事务也访问了该数据,那么在第一个事务修改了这个数据后,第二个事务也修改了这个数据。此时,第一个事务的修改结果就被丢失。
不可重复读
在一个事务内多次读同一数据,在这个事务还没结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改会导致第一个事务两次读取的数据不一样。这就发生了一个事务内两次读到的数据不一样的情况,因此成为不可重复读。
幻读
幻读与不可重复读类似,它发生的是在一个事务读取了多行数据,另一个并发事务插入或删除了一些数据,随后,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读的重点是修改,幻读的重点是新增或删除。
事务隔离的实现
在 MySQL 中,每条记录在更新时都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
不同时刻启动的事务会有不同的 read-view,当系统里没有比回滚日志更早的 read-view 时,会删除该回滚日志。
事务的启动方式
建议通过显式语句启动事务:set autocommit=1
MySQL 的事务启动方式有以下几种:
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。
长事务的危害
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。
查询长事务
在 information_schema 库的 innodb_trx 表查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx
where TIME_TO_SEC(timediff(now(),trx_started))>60
MySQL InnoDB 解决幻读
快照读
普通的 select 都是快照读。
快照读时,通过 MVCC 来避免幻读,是靠保存数据快照来实现
MVCC 多版本并发控制
空间换时间,通过版本号使得大多数读操作无锁化
以 InnoDB 为例,都增加了创建时间、删除时间。具体用系统版本号表示。
具体的版本号(trx_id)存在 information_schema.INNODB_TRX
表中。版本号(trx_id)随着每次事务的开启自增。
事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。
- SELECT
- 创建版本:小于当前版本号
- 删除版本:未设置,或大于当前版本号
- INSERT:将新插入行的创建版本设置为当前事务版本号
- DELETE:将删除行的删除版本设置为当前事务版本号
- UPDATE:
- 插入新纪录,同INSERT操作
- 删除旧记录,同DELETE操作
当前读
读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题
- select ... lock in share mode (共享读锁)
- select ... for update
- update、delete、insert
通过 next-key 来避免幻读,next-key 锁包含两部分:
- 记录锁(行锁)
- 间隙锁
记录锁是加在索引上的锁,间隙锁是加在索引之间的。(思考:如果列上没有索引会发生什么?)
原理:将当前数据行与上一条数据和下一条数据之间的间隙锁定,保证此范围内读取的数据是一致的。