MySQL事务的特点(ACID):
●原子性:对数据库的一系列的操作,要么都是成功,要么都是失败,不可能出现部分成功或者部分失败的情况;原子性,在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作。
●隔离性:在数据库里面会有很多的 事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作, 那么我们对隔离性的定义,就是这些很多个的事务,对表或者行的并发操作,应该是透明的,互相不干扰的。通过这种方式,我们最终也是保证业务数据的一致性。
●持久性:我们对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为我们系统宕机或者重启了数据库的服务器,它又恢复到原来的状态了;持久性是通过 redo log 和 double write 双写缓冲来实现的。
● 一致性:是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。上面三个特性保证了一致性。
事务的实现原理:
事务的原子性是通过 undo log 来实现的
事务的持久性是通过 redo log 来实现的
事务的隔离性是通过(读写锁+MVCC)来实现的
事务的一致性是通过原子性、持久性、隔离性来实现的
事务的隔离级别
读未提交(Read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
●RU 隔离级别:不加锁
读提交(Read committed):一个事务提交之后,它做的变更才会被其他事务看到。
●RC 隔离级别下,普通的 select 都是快照读,使用 MVCC 实现
●加锁的 select 都使用记录锁,因为没有 Gap Lock
●除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间;所以 RC 会出现幻读的问题。
可重复读(Repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
●RR 隔离级别下,普通的 select 使用快照读(snapshot read),底层使用 MVCC 来实现
●加锁的 select(select … in share mode / select … for update)以及更新操作update, delete 等语句使用当前读(current read)(当前读inndb可能会导致幻读问题),底层使用记录锁、或者间隙锁、临键锁。
串行化(Serializable ):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
●Serializable 所有的 select 语句都会被隐式的转化为 select … in share mode,会和update、delete 互斥
事务隔离级别的问题
●脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。
●不可重复读(虚读):指一个线程中的事务读取到了另外一个线程中提交的update/delete的数据。
●幻读:指一个线程中的事务读取到了另外一个线程中提交的insert的数据。
MySQL锁
官网八锁
数据读一致性方案
1、LBCC(锁的算法)
我既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要 操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。 如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那 就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地 影响操作数据的效率。
2、MVCC
如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照 就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control (MVCC)。
MVCC 的核心思想是: 我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。
innodb的行锁是通过给索引项加锁实现的,这就意味着只有通过索引条件检索数据时,innodb才使用行锁,否则使用表锁。
锁的模式
意向锁锁的不是数据,而是告知当前表是否有没有共享锁或者排它锁,用来确保你加锁是否能成功,提高加锁的效率
什么是意向锁
在多层级锁中的事务中,如果事务在某节点持有读写锁,则事务在其父节点持有意向锁。
这样通过判断目标节点的上层节点是否持有意向锁来进一步决定能不能对下级节点加锁。比如要对表中任一节点加写锁,那么就要先获得整张表的意向锁,如果能获得,进一步对行加锁;如果获得失败,说明其他事物正在对这张表进行写,不可以写。通俗的理解就是,你想读写人家的儿子,得知会他老爸一声。
有什么好处
避免不必要的检查目标节点的锁状态,比如发现某张表没能获得意向锁,说明有人在用,事务会等待意向锁的释放,而不是固执地一行一行去检查行有没有被上锁, 避免加锁解锁开销
LBCC 锁算法
●数据库里面存在的主键值,我们把它叫做 Record,记录,那么这里我们就有 4个 Record。
●根据主键,这些存在的 Record 隔开的数据不存在的区间,我们把它叫做 Gap,间隙,它是一个左开右开的区间。间隙锁不排它
● 间隙(Gap)连同它左边的记录(Record),我们把它叫做临键的区间,它是一个左开右闭的区间。临建=记录+间隙
●若果主键为非整数类型,通过用ASCII码来排序。
在我们使用锁的时候,有一个问题是需要注意和避免的,我们知道,排它锁有互斥的特性。一个事务或者说一个线程持有锁的时候,会阻止其他的线程获取锁,这个时候会造成阻塞等待,如果循环等待,会有可能造成死锁。
死锁
锁什么时候释放:事务结束(commit,rollback);客户端连接断开。
如果一个事务一直未释放锁,其他事务会被阻塞多久?会不会永远等待下去?如果是,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占 用大量计算机资源,造成严重性能问题,甚至拖跨数据库。
MVCC
当前读
当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
修改更新数据必须进行当前读 先读后写
这个概念其实很好理解,MySQL加锁之后就是当前读。假如当前事务只是加共享锁,那么其他事务就不能有排他锁,也就是不能修改数据;而假如当前事务需要加排他锁,那么其他事务就不能持有任何锁。总而言之,能加锁成功,就确保了除了当前事务之外,其他事务不会对当前数据产生影响,所以自然而然的,当前事务读取到的数据就只能是最新的,而不会是快照数据,LBCC方案中,如果我们的业务系统是读多写少的话,这种方案就会极大影响了效率,所以我们就有了另一种解决方案:MVCC。
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
快照读
读取的是记录的可见版本 (有可能是历史版本),不用加锁。
快照读是针对上文的当前读而言,指的是在RR隔离级别下,在不加锁的情况下MySQL会根据回滚指针选择从undo log记录中获取快照数据,而不总是获取最新的数据,这也就是为什么另一个事务提交了数据,在当前事务中看到的依然是另一个事务提交之前的数据
简单的select操作,属于快照读,不加锁:
Mvcc 解决了幻读是依据快照读的,但在更新修改数据时,引用的时当前读一定程度上还是存在幻读问题
MVCC分析流程
执行流程
最终结果
结果分析
事务A的row_trx_id=999,依据的是快照读,读取的是快照历史数据
事务B因为update更新操作引发的是当前读,读取的是最新的数据
事务C则是自动提交
MVCC查询规则:
1、只查询事务id小于等于当前事务id的数据。(这里要等于是因为假如自己的事务插入了一条数据,会生成一条当前事务id的数据,所以必须包含本事务自己插入的数据)
2、只查询未删除(回滚指针为空)或者回滚指针大于当前事务id的数据。(这里不能等于是因为假如自己的事务删除了一条数据,会生成数据的回滚指针为当前事务id,所以必须排除掉自己删除的数据)
count(1) 比 count(*) 效率高么?
在高版本的MySQL有了MVCC之后(5.5及以后,5.1的没有考证)是没有什么区别的,也就没有COUN(1)会比COUNT(*)更快这一说了。
MyISAM 引擎会把一个表的总行数记录了下来,所以在执行 count(*) 的时候会直接返回数量,执行效率很高。在 MySQL 5.5 以后默认引擎切换为 InnoDB,InnoDB 因为增加了版本控制(MVCC)的原因,同时有多个事务访问数据并且有更新操作的时候,每个事务需要维护自己的可见性,那么每个事务查询到的行数也是不同的,所以不能缓存具体的行数,他每次都需要 count 一下所有的行数
有 Where 条件的 count,会根据扫码结果count 一下所有的行数,其性能更依赖于你的 Where 条件
COUNT()有两个非常不同的作用:它可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)。如果在COUNT()的括号中定了列或者列表达式,则统计的就是这个表达式有值的结果数。…COUNT()的另一个作用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当我们使用COUNT()的时候,这种情况下通配符并不像我们猜想的那样扩展成所有的列,实际上,他会忽略所有列而直接统计所有的行数
count() 不同,他不关心这个返回值是否为空都会计算他的count,因为 count(1) 中的 1 是恒真表达式,那么 count() 还是 count(1) 都是对所有的结果集进行 count,所以他们本质上没有什么区别
当然这个地方 InnoDB 本身也做了一些优化,它会使用最小的二级索引来进行 count 的查询优化。如果没有二级索引才会选择聚簇索引,这样的设计单从 IO 的角度就节省了很多开销。
到这里我们明白了 count(*) 和 count(1) 本质上面其实是一样的,那么 count(column) 又是怎么回事呢?
count(column) 也是会遍历整张表,但是不同的是它会拿到 column 的值以后判断是否为空,然后再进行累加,那么如果针对主键需要解析内容,如果是二级所以需要再次根据主键获取内容,又是一次 IO 操作,所以 count(column) 的性能肯定不如前两者喽,如果按照效率比较的话:count(*)=count(1)>count(primary key)>count(column)
既然 count(*) 在查询上依赖于所有的数据集,是不是我们在设计上也需要尽量的规避全量 count 呢?通常情况我们针对可预见的 count 查询会做适当的缓存,可以是 Redis,也可以是独立的 MySQL count 表,当然无论是哪种方式我们都需要考虑一致性的问题。
自增 ID 用完了怎么办 ?
数据表定义的自增 ID,如果达到上限之后,再申请下一个 ID 的时候,获得到的值将保持不变.
●将Int类型改为BigInt类型 8 个字节
insert 语句插入数据成功后,这个表的 AUTO_INCREMENT 没有改变(还是 4294967295),就导致了第二个 insert 语句又拿到相同的自增 id 值,再试图执行插入语句,报主键冲突错误
●InnoDB 系统自增 row_id 6 个字节
如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。
实际上,在代码实现时 row_id 是一个长度为 8 字节的无符号长整型 (bigint unsigned)。但是,InnoDB 在设计时,给 row_id 留的只是 6 个字节的长度,这样写到数据表中时只放了最后 6 个字节,所以 row_id 能写到数据表中的值,就有两个特征:
row_id 写入表中的值范围,是从 0 到 2^48-1
当 dict_sys.row_id=2^48时,如果再有插入数据的行为要来申请 row_id,拿到以后再取最后 6 个字节的话就是 0。
也就是说,写入表的 row_id 是从 0 开始到 2^48-1。达到上限后,下一个值就是 0,然后继续循环。
当然,2^48-1 这个值本身已经很大了,但是如果一个 MySQL 实例跑得足够久的话,还是可能达到这个上限的。在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。
●thread_id
线程 id 才是 MySQL 中最常见的一种自增 id
系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。
thread_id_counter 定义的大小是 4 个字节,因此达到 2^32-1 后,它就会重置为 0,然后继续增加。但是,你不会在 show processlist 里看到两个相同的 thread_id
每种自增 id 有各自的应用场景,在达到上限后的表现也不同:
表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。
row_id 达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据。
Xid 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。
InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的 bug,好在留给我们的时间还很充裕。
thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。