MySQL事务
<h2>一、什么是事务?</h2>
<p>事务要保证一组数据库操作,要么全部成功,要么全部失败。在mysql中事务是在存储引擎层实现的,InnoDB支持事务,通过 begin / start transaction 显式开启一个事务,直到第一个操作库表的语句,事务才会实际开启,配套的提交语句是 commit,回滚语句是 rollback。</p>
<h2>二、事务的四大特性?</h2>
<p><img src="https://pic3.zhimg.com/80/v2-fc75f36cb0555d66ebbb4d2c6613f576_1440w.webp" alt="" /></p>
<h3>1.原子性</h3>
<p>Atomicity(原子性):事务是一个整体,要么全部完成,或者全部不完成,不会有中间状态。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态。</p>
<p>原子性在数据库中的体现就是<strong>事务回滚</strong>,回滚能够撤销所有已经执行的sql语句,InnoDB实现回滚靠的是<strong>回滚日志undo log。</strong>在mysql中,每次更新记录都会先插入一条 undo log 并且持久化,undo log 通过回滚事务指针形成了链表。当系统崩溃时,扫描没有 commit 的事务对应的 undo log,按照类型执行回滚操作</p>
<ul>
<li>insert 类型:undo log 记录了 id ,根据 id delete</li>
<li>delete 类型:undo log 记录了删除的数据,执行 insert</li>
<li>
<p>update 类型:undo log 记录了修改前的数据,执行反向 update</p>
<p>INSERT INTO t (id, a) VALUES (1, “A”);
UPDATE t SET a=“B” WHERE id = 1;
UPDATE t SET a=“C” WHERE id = 1;</p>
</li>
</ul>
<p><img src="https://pic1.zhimg.com/80/v2-413448a9aad85c630406425eb1f04ea4_1440w.webp" alt="" /></p>
<p>事务3 回滚 ,会将数据回滚到事务2修改后,而事务2想回滚它会将数据回滚到事务1插入后的状态,事务1回滚就会删除插入的记录。</p>
<h3>2.持久性</h3>
<p>持久性:对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。</p>
<p>Mysql为了提高性能,使用了 BufferPool ,读数据时如果内存中不存在,会从磁盘读取到内存 BufferPool 中,增、删、改等操作会先修改 BufferPool 中的数据页,这时候内存中的数据和磁盘不一致,出现了脏页,Mysql 通过其他的机制会将脏页刷到磁盘。</p>
<p>但是万一脏页还没刷到磁盘,Mysql 宕机就会导致数据丢失的问题,Mysql 通过 redolog(重做日志)解决了这个问题,保证了持久性。</p>
<p>Mysql 在 更新记录写入 BufferPool 之前会把记录 先写到 redolog (Write Ahead Logging ),当事务提交时会先将 redolog 持久化到磁盘,如果出现宕机,重启后将 redolog 中的事务重放即可。</p>
<p>> redo log 是物理日志 内容是 对 XXX表空间中的XXX数据页XXX偏移量的地方做了XXX更新</p>
<p>同样的为了提高性能,redolog 也是会先写 redolog buffer ,但事务提交时会将涉及的 redolog 写到磁盘,所以不会有持久性问题。</p>
<p><img src="https://pic4.zhimg.com/80/v2-c41ac6b95152570e7465ad21c2a26f83_1440w.webp" alt="" /></p>
<p>redolog 的刷盘策略</p>
<ul>
<li>innodb_flush_log_at_trx_commit=0:每秒将buffer中的数据刷到磁盘 flush+fsync</li>
<li>innodb_flush_log_at_trx_commit=1:每次提交 刷到磁盘 flush+fsync</li>
<li>innodb_flush_log_at_trx_commit=2:每次提交 刷到系统文件缓存 flush</li>
<li>redolog buffer 占用空间(innodb_log_buffer_size 默认8MB)达到一半 刷到文件缓存 flush</li>
</ul>
<p>因为刷盘是会将redolog buufer 全部持久化到磁盘,如果事务还没有提交,也是有可能被持久化到磁盘的</p>
<h3>3.隔离性</h3>
<p>隔离性是指事务之间应该是隔离的,并发执行的各个事务之间不能互相干扰,在没有隔离性约束下,并发事务就可能出现脏读、不可重复读、幻读的问题,为了解决这些问题,就有了“隔离级别”的概念。</p>
<ul>
<li>读未提交:一个事务还没提交,它的修改就能被别的事务看到。</li>
<li>读提交:一个事务提交之后,它的修改才会被其他事务看到。(解决了脏读)</li>
<li>可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。(解决了脏读、不可重复读)</li>
<li>串行化:在该级别下,写会加写锁、读会加读锁,除了读读不互斥,其他组合都互斥,因此可以保证事务串行化顺序执行。(解决了脏读、不可重复读与幻读)</li>
</ul>
<p>读未提交没有隔离性,串行化是通过锁来实现的,强制事务顺序执行性能比较差,一般也不会用。mysql中的读提交和可重复读是通过<strong>MVCC</strong>来实现的。</p>
<p>MVCC多版本并发控制提供了一致性读,也叫快照读,事务在执行快照读时会对整个数据库当前的事务进行一个快照,并结合 undo log链表,就可以判断当前事务可见哪个版本的数据。</p>
<p><img src="https://pic2.zhimg.com/80/v2-1ad6f9e8f68055a7a0983ed238ec7739_1440w.webp" alt="" /></p>
<p>判断的规则如下:只有在视图创建前提交的版本,当前事务才能可见</p>
<p>假设有如下三个事务 ,事务A id = 6,事务B id = 7 ,事务C id = 8,在A事务之前有个事务 5 将 k 更新成1 并提交;</p>
<p><img src="https://pic3.zhimg.com/80/v2-9e3d07d98925d17132acffd6c0f2a746_1440w.webp" alt="" /></p>
<p>undo log 链如下:</p>
<p><img src="https://pic2.zhimg.com/80/v2-a6dafb90329737558e1e27e2591da129_1440w.webp" alt="" /></p>
<p>在RR的隔离级别,在事务开启时就会创建一致性视图,之后事务的查询都公用这个一致性视图 </p>
<p><img src="https://pic1.zhimg.com/80/v2-44c919a58fddb112c01fbd9ee860aba8_1440w.webp" alt="" /></p>
<p>可重复读</p>
<p>可以看到事务A 读到的数据 是 k = 1,其他事务的修改是不可见的,这里需要注意 update 操作使用的是当前读,读到的是当前读,select 如果加锁 ,也是当前读。</p>
<p>> MVCC 快照读解决了幻读问题,对于当前读的 幻读问题 是通过 next-key lock 间隙锁保证了数据读取期间,其他事务不会在该间隙内增加数据,解决幻读问题。</p>
<p>在RC的隔离级别,每个查询语句执行前创建一致性视图。</p>
<p><img src="https://pic1.zhimg.com/80/v2-d6bf53f24a55e9cd215f0a0ed0ea4ee8_1440w.webp" alt="" /></p>
<p>读提交</p>
<h3>4.一致性</h3>
<p>一致性指的是数据库业务数据的正确性,事务保证只能把数据库从一个有效(正确)的状态转移到另一个有效(正确)的状态,业务数据属于应用层,所以这个正确状态需要应用层去定义,AID 可以协助我们去实现这个正确状态的定义</p>
<p>php事务的并发问题</p>
<p>1、脏读
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=588e22e6b05ca00c14c17a9a34647749&amp;file=file.png" alt="" /></p>
<p>事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。</p>
<p>2、不可重复读</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=df6c1083ec6ef8a792a2e18c35fec9d5&amp;file=file.png" alt="" /></p>
<p>事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。</p>
<p>3、幻读
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=1a959f8cdfea2b5dd7bc78305a03b4de&amp;file=file.png" alt="" /></p>
<p>系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。</p>
<p>幻读:原因:因为mysql数据库读取数据时,是将数据放入缓存中,当事务B对数据库进行操作:例如删除所有数据且提交时,事务A同样能访问到数据,这就产生了幻读。
问题:解决了可重复读,但是会产生一种问题,错误的读取数据,对于其他事务添加的数据也将访问不到</p>
<p>有一个“锁”的机制:行锁和表锁:InnoDB默认是行锁,如果在事务操作的过程中,没 有使用到索引,没有使用到索引,那么系统会自动全表检索,自动升级为表锁。 § 行锁:只有当前行被锁住,别的用户不能操作</p>
<p>小结:<br />
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。</p>
<h1>1. MySQL事务</h1>
<p>mysql事务特性:</p>
<pre><code> 1) 原子性
2) 一致性
3) 隔离性
4) 持久性</code></pre>
<p>golang MySQL事务应用:</p>
<pre><code> 1) import (“github.com/jmoiron/sqlx&quot;)
2) Db.Begin() 开始事务
3) Db.Commit() 提交事务
4) Db.Rollback() 回滚事务
package main
import (
&quot;fmt&quot;
_ &quot;github.com/go-sql-driver/mysql&quot;
&quot;github.com/jmoiron/sqlx&quot;
)
type Person struct {
UserId int `db:&quot;user_id&quot;`
Username string `db:&quot;username&quot;`
Sex string `db:&quot;sex&quot;`
Email string `db:&quot;email&quot;`
}
type Place struct {
Country string `db:&quot;country&quot;`
City string `db:&quot;city&quot;`
TelCode int `db:&quot;telcode&quot;`
}
var Db *sqlx.DB
func init() {
database, err := sqlx.Open(&quot;mysql&quot;, &quot;root:root@tcp(127.0.0.1:3306)/test&quot;)
if err != nil {
fmt.Println(&quot;open mysql failed,&quot;, err)
return
}
Db = database
}
func main() {
conn, err := Db.Begin()
if err != nil {
fmt.Println(&quot;begin failed :&quot;, err)
return
}
r, err := conn.Exec(&quot;insert into person(username, sex, email)values(?, ?, ?)&quot;, &quot;stu001&quot;, &quot;man&quot;, &quot;stu01@qq.com&quot;)
if err != nil {
fmt.Println(&quot;exec failed, &quot;, err)
conn.Rollback()
return
}
id, err := r.LastInsertId()
if err != nil {
fmt.Println(&quot;exec failed, &quot;, err)
conn.Rollback()
return
}
fmt.Println(&quot;insert succ:&quot;, id)
r, err = conn.Exec(&quot;insert into person(username, sex, email)values(?, ?, ?)&quot;, &quot;stu001&quot;, &quot;man&quot;, &quot;stu01@qq.com&quot;)
if err != nil {
fmt.Println(&quot;exec failed, &quot;, err)
conn.Rollback()
return
}
id, err = r.LastInsertId()
if err != nil {
fmt.Println(&quot;exec failed, &quot;, err)
conn.Rollback()
return
}
fmt.Println(&quot;insert succ:&quot;, id)
conn.Commit()
}</code></pre>
<p>输出结果:</p>
<pre><code> insert succ: 2
insert succ: 3</code></pre>
<p>查看MySQL:</p>
<pre><code> mysql&gt; select * from person;
+---------+----------+------+--------------+
| user_id | username | sex | email |
+---------+----------+------+--------------+
| 2 | stu001 | man | stu01@qq.com |
| 3 | stu001 | man | stu01@qq.com |
+---------+----------+------+--------------+
2 rows in set (0.00 sec)</code></pre>