MySQL——剖析事务

MySQL——事务

一、事务的基本概念

  • 事务(Transaction)是一个操作序列——这些操作要么都做,要么都不做,是一个不可分割的工作单位,是数据库中的逻辑工作单位
  • 事务是为了保证数据一致性,保证数据库的完整性
  • 事务间不能相互嵌套

二、事务的ACID属性

事务的四大特征——原子性、一致性、隔离性和持久性

1、原子性(Atomicity)

一个事务的原子性的含义就是一组操作要么全部完整执行,要么干脆全都不执行。这意味着,工作单元中的每项任务都必须正确执行,如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。

2、一致性(Consistency)

一致性代表了底层数据存储的完整性,它必须由事务系统和应用开发人员共同来保证

事务系统通过保证事务的原子性、隔离性和持久性来保证数据的一致性;

应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即数据预期所表达的现实业务情况不相一致)

例如,事务的一致性要求在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等

支付宝账号100 你读到余额要取,有人向你转100 但是事务没提交(这时候你读到的余额应该是100,而不是200) 这种就是一致性

对于数据库的操作,一定要保证结束之后数据从一个一致性到另一个一致性

3、隔离性(Isolation)

隔离性要求各个事务之间相互不会产生影响

隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响

所以一个事务是不会对另一个事务产生影响的,在一个事务的所有操作还没结束之前,外界是不能观察到此事务产生的数据变化的

由于隔离性的控制强弱产生了如下四种从低到高的隔离级别:

  • 读未提交
  • 读已提交
  • 不可重复度
  • 序列化

其中序列化是最安全的一种方式,不会产生问题。但是严格的隔离级别会导致效率变低,在某些情况下为了提高程序的执行效率,会在系统能够接收的数据不一致的前提下,降低隔离级别,不同的隔离级别会产生的数据不一致的情况如下:

脏读 不可重复读 幻读
read uncommitted
read committed
repeatable read
serializable

4、持久性(Durability)

持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备(磁盘),这样可以保证所作的修改在任何系统瘫痪时不至于丢失

三、不同的隔离级别产生的不一致现象

单线程串行执行情况下隔离级别是不会产生影响的,我们提起隔离级别往往就意味着是在并发情况下。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户的并发操作,但与此同时,会带来数据不一致问题,不同的隔离级别对应的数据不一致问题:

脏读 不可重复读 幻读
read uncommitted
read committed
repeatable read
serializable

1、读未提交——脏读

脏读又称无效数据读出,就是一个事务读取到另一个还未提交的事务的数据

脏读是指一个事务正在对数据进行修改,在此事务提交之前,理论上来说别的事务是不应该观察到此事务修改的数据的情况的。但是如果隔离级别是读已提交,则另一个事务是能够观察到这个还未提交的事务产生的数据变化的,这就叫 “脏读”

如果另一个事务读取到“脏的数据”,并依据此数据做进一步的处理,就会产生未提交的数据的依赖关系,这样一旦上一个事务产生异常,进行事务回滚,则数据依赖关系的处理就会产生错误,就会产生数据的不一致问题

比如你账户还有 500 块钱,你老婆给你打 1000 块生活费,不过你老婆只输入了金额还没点击提交,此时如果你能观察到这 1000 块钱,总余额为 1500 块,你就会取 1500 块,可是你老婆突然觉得 500 块够你花了,又不给你打了,此时就会产生错误

2、读已提交——不可重复读

正常情况下在同一个事务内部,相同的查询语句不管执行多少次查询到的结果应该是相同的

不可重复读就是在同一个事务内,两个相同的查询返回了不同的结果

比如一个事物内部有多个查询语句,其中有两个相同的查询语句,在此事务还未结束之前,我认为这两条查询语句查询到的结果应该是相同的,但是在读已提交的隔离级别之下,一旦在这两条相同的SQL语句执行的中间,另一个事务把数据进行了修改或删除,则两条相同的SQL语句查询到的结果是不同的。也就是说,无法重复读取到相同的数据,这就是“不可重复读”

比如你在银行查询余额还剩5000,过了两秒你再进行查询发现就剩2000了,这肯定是不正常的。正常情况是,你在查询余额操作的这段时间,是不能有别的事务操作你的账户的

3、可重复读——幻读

一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”

幻读就是查不到数据,但是数据本身是存在的,当你查询数据时没有数据,但是当你插入数据的时候,发现数据是存在的,不能再插入重复数据了

4、总结对比

各种隔离级别产生的数据不一致现象的对比:

  • 脏读是针对未提交的数据
  • 不可重复读是针对其他事务提交前后,读取数据本身的对比——重点在于对已存在数据的修改
  • 幻读是针对其他事务提交前后,读取数据条数的对比——重点在于新增或者删除(数据条数变化)

不可重复读和幻读的区别:

  • 单单从查询来看,不可重复读和幻读效果一样
  • 幻读是指在插入和修改数据的时候会产生幻读,查询数据是不会产生幻读的

5、隔离性是怎么实现的?

事务的隔离性是怎么实现的?——通过 MySQL 的锁机制来实现的

四、事务是否提交产生的影响

执行 commit 进行事务提交或 callback 进行事务回滚前数据的状态

  • 以前的数据可恢复
  • 当前的用户可以看到DML操作的结果
  • 其他用户不能看到DML操作的结果
  • 被操作的数据被锁住,其他用户不能修改这些数据

执行 commit 进行事务提交后数据的状态

  • 数据的修改被永久写在数据库中
  • 数据以前的状态永久性丢失
  • 所有的用户都能看到操作后的结果
  • 记录锁被释放,其他用户可操作这些记录

执行 callback 进行事务回滚后数据的状态

  • 语句将放弃所有的数据修改
  • 修改的数据被回退
  • 恢复数据以前的状态
  • 行级锁被释放

参考:https://mp.weixin.qq.com/s/n9v9a9u_cJgs8-zd3ZWUew
MySQL21问

想进大厂,mysql不会那可不行,来接受mysql面试挑战吧,看看你能坚持到哪里?

1. 能说下 myisam 和 innodb的区别吗?

myisam 引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且 myisam 不支持外键,并且索引和数据是分开存储的。

innodb 是基于聚簇索引建立的,和 myisam 相反它支持事务、外键,并且通过 MVCC 来支持高并发,索引和数据存储在一起

2. 说下 mysql的索引有哪些吧,聚簇和非聚簇索引又是什么?

索引按照数据结构来说主要包含B+树和Hash索引。

假设我们有张表,结构如下:

create table user(
 id int(11) not null,
  age int(11) not null,
  primary key(id),
  key(age)
);

B+树是左小右大的顺序存储结构,节点只包含id索引列,而叶子节点包含索引列和数据,这种数据和索引在一起存储的索引方式叫做聚簇索引,一张表只能有一个聚簇索引。假设没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有的话则会隐式定义一个主键作为聚簇索引。

在这里插入图片描述

这是主键聚簇索引存储的结构,那么非聚簇索引的结构是什么样子呢?非聚簇索引(二级索引)保存的是主键id值,这一点和myisam保存的是数据地址是不同的。

最终,我们一张图看看InnoDB和Myisam聚簇和非聚簇索引的区别

在这里插入图片描述

3. 那你知道什么是覆盖索引和回表吗?

覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。

而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。

以上面的user表来举例,我们再增加一个name字段,然后做一些查询试试。

explain select * from user where age=1; //查询的name无法从索引数据获取
explain select id,age from user where age=1; //可以直接从索引获取

4. 锁的类型有哪些呢

mysql锁分为共享锁排他锁,也叫做读锁和写锁。

读锁是共享的,可以通过 lock in share mode 实现,这时候只能读不能写。

写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁行锁两种。

表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如 alter 修改表结构的时候会锁表。

行锁又可以分为乐观锁悲观锁,悲观锁可以通过 for update 实现,乐观锁则通过版本号实现。

5. 你能说下事务的基本特性和隔离级别吗?

事务基本特性ACID分别是:

原子性指的是一个事务中的操作要么全部成功,要么全部失败。

一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如 A 转账给 B100块钱,假设中间 sql 执行过程中系统崩溃 A 也不会损失 100 块,因为事务没有提交,修改也就不会保存到数据库。

隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。

持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。

而隔离性有4个隔离级别,分别是:

read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。

用户本来应该读取到 id=1 的用户 age 应该是 10,结果读取到了其他事务还没有提交的事务,结果读取结果 age=20,这就是脏读。

read commit 读已提交,两次读取结果不一致,叫做不可重复读。

不可重复读解决了脏读的问题,他只会读取已经提交的事务。

用户开启事务读取 id=1 用户,查询到 age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。

在这里插入图片描述

repeatable read 可重复复读,这是 mysql 的默认级别,就是每次读取结果都一样,但是有可能产生幻读。

serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

6. 那ACID靠什么保证的呢?

A原子性由 undo log 日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的 sql

C一致性一般由代码层面来保证

I隔离性由 MVCC 来保证

D持久性由内存+redo log来保证,mysql 修改数据同时在内存和 redo log 记录这次操作,事务提交的时候通过 redo log 刷盘,宕机的时候可以从redo log恢复

7. 那你说说什么是幻读,什么是MVCC?

要说幻读,首先要了解MVCC,MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。

我们每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。

还是拿上面的user表举例子,假设我们插入两条数据,他们实际上应该长这样。

id name create_version delete_version
1 张三 1
2 李四 2

这时候假设小明去执行查询,此时current_version=3

select * from user where id<=3;

同时,小红在这时候开启事务去修改id=1的记录,current_version=4

update user set name='张三三' where id=1;

执行成功后的结果是这样的

id name create_version delete_version
1 张三 1
2 李四 2
1 张三三 4

如果这时候还有小黑在删除id=2的数据,current_version=5,执行后结果是这样的。

id name create_version delete_version
1 张三 1
2 李四 2 5
1 张三三 4

由于MVCC的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样

select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);

所以小明最后查询到的id=1的名字还是’张三’,并且id=2的记录也能查询到。这样做是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务自己插入或者修改的

明白MVCC原理,我们来说什么是幻读就简单多了。举一个常见的场景,用户注册时,我们先查询用户名是否存在,不存在就插入,假定用户名是唯一索引。

  1. 小明开启事务current_version=6查询名字为’王五’的记录,发现不存在。
  2. 小红开启事务current_version=7插入一条数据,结果是这样:
id Name create_version delete_version
1 张三 1
2 李四 2
3 王五 7
  1. 小明执行插入名字’王五’的记录,发现唯一索引冲突,无法插入,这就是幻读。

8. 那你知道什么是间隙锁吗?

间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。我们还是以user举例,假设现在user表有几条记录

id Age
1 10
2 20
3 30

当我们执行:

begin;
select * from user where age=20 for update;

begin;
insert into user(age) values(10); #成功
insert into user(age) values(11); #失败
insert into user(age) values(20); #失败
insert into user(age) values(21); #失败
insert into user(age) values(30); #失败

只有10可以插入成功,那么因为表的间隙mysql自动帮我们生成了区间(左开右闭)

(negative infinity,10],(10,20],(20,30],(30,positive infinity)

由于20存在记录,所以(10,20],(20,30]区间都被锁定了无法插入、删除。

如果查询21呢?就会根据21定位到(20,30)的区间(都是开区间)。

需要注意的是唯一索引是不会有间隙索引的。

9. 你们数据量级多大?分库分表怎么做的?

首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。

垂直分库

基于现在微服务拆分来说,都是已经做到了垂直分库了

在这里插入图片描述

垂直分表

如果表字段比较多,将不常用的、数据较大的等等做拆分

在这里插入图片描述

水平分表

首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。

比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。

10. 那分表后的ID怎么保证唯一性的呢?

因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑:

  1. 设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。
  2. 分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种
  3. 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。

11. 分表后非sharding_key的查询怎么处理呢?

  1. 可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。
  2. 打宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如es提供查询服务。
  3. 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。
List<Callable<List<User>>> taskList = Lists.newArrayList();
for (int shardingIndex = 0; shardingIndex < 1024; shardingIndex++) {
    taskList.add(() -> (userMapper.getProcessingAccountList(shardingIndex)));
}
List<ThirdAccountInfo> list = null;
try {
    list = taskExecutor.executeTask(taskList);
} catch (Exception e) {
    //do something
}

public class TaskExecutor {
    public <T> List<T> executeTask(Collection<? extends Callable<T>> tasks) throws Exception {
        List<T> result = Lists.newArrayList();
        List<Future<T>> futures = ExecutorUtil.invokeAll(tasks);
        for (Future<T> future : futures) {
            result.add(future.get());
        }
        return result;
    }
}

12. 说说mysql主从同步怎么做的吧?

首先先了解mysql主从同步的原理

  1. master提交完事务后,写入binlog
  2. slave连接到master,获取binlog
  3. master创建dump线程,推送binglog到slave
  4. slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
  5. slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
  6. slave记录自己的binglog

在这里插入图片描述

由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/108726213
今日推荐