MySQL事务
MySQL事务机制主要用于处理操作量大、复杂度高的数据
- 在MySQL中只有使用了Innodb数据库引擎的数据表和数据库才支持事务
- 事务处理可以用来维护数据的完整性,保证多条SQL语句要么全部执行,要么全部不执行
- 事务用于管理insert、update和delete之类的DML语句,[select语句],其它类型的SQL语句没有事
务的概念
概述事务
事务必须满足ACID4个条件:A原子性、C一致性、I隔离性、D持久性
- 原子性:一个事务中的所有操作要么全部完成、要不一个都不做,不会结束在中间某个环节
- 一致性:事务执行结束后数据库的完整性没有破坏
- 隔离性:数据库允许多个并发事务对数据库中的数据进行读写操作,隔离性可以防止多个事务并发执行时导致的数据不一致性。事务根据隔离等级可以分为4级:读未提交、读已提交、可重复读和串行化
- 持久性:事务执行完成后对数据的修改就是永久的
在MySQL命令行的默认设置下,事务都是自动提交的。如果需要使用事务则需要显式的开启事务
start transaction或者begin,或者执行命令set autocommit=0,用来禁止使用当前会话的自动提交
delete from和tuncate table的区别
事务与数据库底层数据
事务的进行过程中,在未结束之前,DML语句并不会直接更改底层数据,只是将历史操作记录一下,在内存中完成记录。只有在事务结束时,而且应该是成功结束时,才会修改底层硬盘文件中的数据
- 事务的原子性是通过undo log来实现
- 事务的持久性是通过redo log来实现
- 事务的隔离性是通过【读写锁+MVCC多版本并发控制】来实现的
- 事务的一致性是通过原子性、持久性和隔离性来实现的
事务控制语句
- begin或者 start transaction可以显式的开启一个事务,结束事务有提交和回滚2种方式
- commit提交事务,并使已执行的对数据库的所有修改成为永久性修改
- rollback回滚结束事务,撤销已经执行的未提交的修改操作
- savepoint 标识名称 用于在事务过程种创建一个保存点,从而支持部分回滚。一个事务中可以添
加多个保存点 - release savepoint 标识名 用于删除一个事务的保存点,如果对应名称的保存时不存在则抛出异常
- rollback to 标识名 将事务回滚到指定的保存点,执行名称的保存点到当前位置的所有操作撤销,
但是保存点之前的操作仍旧保留,等待事务结束 - set transaction isolation level用于设置事务的隔离性,innodb存储引擎提供的隔离性有读未提交
read uncommitted、读已提交read committed、可重复读repeatable read和serializable串行化,系统默认隔离等级为可重复读
事务处理
begin或者start transaction开启事务 rollback事务回滚 commit事务提交
还可以使用set改变MySQL的自动提交模式
- set autocommit=0 禁止自动提交
- set autocommit=1 开启自动提交
基本测试
在navicat或者命令行中开启两个窗口模拟两个并发修改数据库的进程
1、创建数据库 create database test1;
2、切换当前库 use test1;
3、创建测试使用的表 create table t1(id int primary key,name varchar(32))engine=innodb;
MySQL8默认数据库的存储引擎就是innodb
4、开启事务: begin;
5、插入操作: insert into t1 values(1,'zhangsan');
,在另外一个窗口中执行查询,则看不到插
入的数据,因为事务的默认隔离等级为可重复读
6、提交事务 commit; 修改生效,另外一个窗口中则能够查询到数据
7、如果没有提交还可以使用rollback撤消修改操作
多点回滚
begin;
update t1 set name='modify1' where id=1;
select * from t1;
savepoint s1;
delete from t1;
rollback to s1;
select * from t1;
commit;
相关日志问题
- 事务的原子性是通过undo log来实现
- 事务的持久性是通过redo log来实现
redo log
如果每次读写数据都需要磁盘的IO,效率会很低。innodb提供了缓存buffer pool作为访问数据库的缓
存,读取和修改操作都会涉及到缓存的操作,缓存会定期刷新到磁盘中,但是写入缓存的数据在系统宕机时会丢失,事务的持久性则无法保证。每次读写硬盘数据的IO成本太高,为了解决这个问题,引入了redo log来提升更新数据的执行效率。
当事务提交时,先将redo log buffer写入redo log文件进行持久化,待事务commit操作完成时才算完
成。这种作为成为预先日志持久化write-ahead log。在持久化一个数据页之前,先将内存中相应的日志页持久化。当有一条数据需要更新时,innodb会将记录写入到redo log中,并更新内存,这时更新就算完成。innodb会在适当的时候,例如系统空闲时,才真正将操作记录更新到磁盘。如果在数据落盘之前系统宕机,数据库重启后,可以通过日志来保证数据的完整性
undo log
undo log提供了两个作用:提供回滚和多版本控制MVCC
在数据修改时不仅记录redo,还记录了相对应的undo。undo log主要记录的是数据的逻辑变化,为了
在发生错误时回滚之前的所有操作。
undo日志用于将数据库逻辑的恢复到原来的样子,所以实际上记录的时相反的工作。例如insert对应的
是delete。undo日志用于事务的回滚操作,进而保证了事务的原子性
隔离级别
数据库重要的功能就是实现数据共享,对于同时运行的多个事务,当多个事务同时访问数据库中相同的数据时,如果没有采取必要的隔离机制,则会导致出现各种并发问题。
- 问题的本质就是共享数据的线程安全问题
常见问题
1、第一类丢失更新:A事务回滚时把已经提交的B事务更新的数据覆盖了。解决方案是锁机制
2、脏读:A事务读取到B事务更新但是还没有提供的数据,如果B回滚撤销,则A读取的数据就是临时而且无效的数据。
3、不可重复读:A事务读取到了一个字段值,但是B更新并提交了该字段的修改,A再次读取同一个字段值,但是两次读取到的内容不一致
4、幻读:A事务从一个表中读取了多行数据,但是B事务插入或者删除了一些新的行,如果A再次读取,则发现数据会有多出来或者少掉的行
5、第二类丢失更新:A事务修改记录,同时B事务修改记录,B提交数据后使用B的修改结果覆盖了事务A的修改结果
事务隔离性
数据库系统必须具有隔离并发各个事务的能力,使其相互不会影响,避免各种并发问题。一个事务和其它事务隔离的程度就成为隔离等级。数据库中规定了多种事务隔离级别。不同的隔离级别对应不同的干扰程度,隔离级别越到,数据的一致性就越号,但是并发性越差
MySQL数据库支持4种隔离级别,默认可重复读
隔离级别的范围
隔离级别的作用范围可以分为全局级和会话级两种。全局级对所有的会话有效,会话级只对当前会话有效
设置全局隔离等级 set global transaction isolation level read committed;
设置会话级隔离等级 set session transaction isolation level read uncommitted;
总结隔离等级
隔离级别 | 描述 |
---|---|
Read-Uncommitted | 允许事务读取其它事务没有提交的数据,脏读、不可重复读和幻读问题都会出现 |
Read-Committed | 只允许事务读取其它事务已经提交的数据,可以避免脏读,但是不可重复读和幻读问题都会出现 |
Repeatable-Read | 可以保证多次从一个字段中读取相同的数据,可以认为事务开启时会自动对现有数据进行快照,其它事务修改不管是否提交,当前事务读取的时快照数据,可以避免脏读和不可重复读,但是幻读问题会出现。快照是VCC多版本并发控制 |
Serializable | 可以确保事务是串行执行,可以避免所有的并发问题,但是由于性能低下,一般不使用 |
在具体应用开发中,一般建议使用数据库管理系统默认的隔离等级,同时在编程中引入乐观锁
样例:两个事务同时操作tb_users表中的age值
读未提交
MySQL数据库中事务的隔离实际上是依靠锁机制来实现的,但是加锁会带来性能的损失。读未提交隔离等级是不加锁的,所以性能最好,但是由于基本没有什么限制,所以脏读问题都无法解决。
读已提交
解决脏读问题的方法就是只允许读取别的事务已经提交的数据,其它事务未提交数据当前事务不能读
取。例如oracle默认的事务隔离级别就是读已提交。由于只能读取已经提交的数据,所以可能出现两次读取的数据不一致
可重复读
针对不可重复读的问题提出了可重复读的隔离等级,针对查询采用了MVCC多版本并发控制引入快照机制,每个事务都有自己的数据快照,即使其它事务提交数据,也不影响当前事务相关行的数据快照。幻读仍旧会出现
为了解决不可重复读的问题,MySQL采用了MVCC多版本并发控制的方式。数据库中的一行记录实际上有多个版本,每个版本除了有数据之外,还有一个标识版本的字段row trx_id,这个字段就是产生的对应事务的id,在事务开始的时候向事务系统申请,按照时间先后顺序递增
一行记录现在有3个不同的版本,每个版本中都记录了使其产生的事务id,每个数据存储的备份就是快照【一致性视图】,可重读读就是在事务开始的时候生成一个当前事务的全局性的快照,但是读提交则是每次执行语句时都会重新生成一次快照。
读取快照数据的规则:
- 版本未提交不能读取
- 版本已经提交,但是却是在快照创建后提交的,不能读取
- 版本已经提交,且是在快照创建前提交的,则可以读取
可重复读和读已提交两者主要的区别在于快照的创建上,可重复读仅仅在事务开始时创建一次,而读已提交每次执行sql语句时都要创建一次
串行化
隔离等级最高,隔离效果最好,可以解决脏读、不可重复度和幻读问题,当时并发性最差。将事务的并发执行转换为顺序执行,后一个事务的执行必须等待前一个事务结束
并发写问题
事务A执行update操作,update时要对所修改的数据行进行加锁,这个行所在事务提交后才能释放,而在事务A提交之前,如果事务B也希望修改这行数据,必须先进行行锁的申请,但是由于A已经占用了行锁,所以B申请不到,此时B一直会处于等待状态,直到A提交释放锁后,B才能执行
update tb_users set age=10 where id=1;
id就是主键PK,是有索引的,那么MySQL在索引树种查找到这行数据,然后加上行锁
update tb_users set age=20 where age=10;
假设表种并没有针对age设置索引,所以MySQL无法直接定位这行数据。MySQL会给这个表种的所有行加锁,但是添加行锁后,MySQL会再次执行一次过滤,发现不满足条件的行就释放锁,最后只留下符合条件的行。但是一次锁定一次解锁的过程对性能影响比较大,如果是大表的化,还是建议合理设计索引
幻读问题
解决并发问题的方案就是行锁,解决幻读也是依赖于锁机制实现,使用间隙锁。MySQL把行锁和间隙锁合并在一起,就可以解决并发写和缓读问题,这个锁叫做next-key锁
例如: select * from tb_student
可以获取age=10和age=30的数据,针对索引数据库会创建维护一
个B+树,树可以用来快速定位行记录
针对具体的行数据,例如age=10和age=30的数据,添加一个行锁,根据age=10和age=30可以将整个区间划分为3部分,(负无穷大,10]、(10,30)和[30,正无穷大)三个部分,在这3个区间上可以添加间隙锁
事务A | 事务B |
---|---|
begin | begin |
select * from tb_users | |
update tb_users set name=‘zhangsan’ where age=10 | |
insert into tb_users values(null,‘lisi’,10); | |
select * from tb_users where age=10解决幻读问题 | |
commit | commit |
在事务A提交之前,事务B的插入操作只能等待,这实际上就是间隙锁生效。
- 如果表中有索引,实际上直接可以使用行锁,如果不是索引列,那么数据库会为整个表加上间隙
锁。 - MySQL的innodb引擎才能支持事务,其中可重复读是MySQL默认的隔离级别
- 读未提交和串行化基本上不需要考虑隔离级别,因为读未提交不加锁限制;串行化相当于单线程执行,效率太差
- 读已提交解决了脏读问题,行锁解决了并发更新问题,并且MySQL在可重复读时引入行锁+间隙锁的组合可以实现可重复读
悲观锁和乐观锁
悲观锁:获取数据时都会直接加锁,共享资源每次只给一个线程使用,其它线程阻塞等待。在数据库中提供了行锁、表锁等,操作数据时先加锁后使用。例如售票系统 select * from ticket where id=100 for update
乐观锁:不是数据库系统自带的,需要开发实现。乐观锁是只操作数据时并不进行任何特殊处理,也就是不加锁,在进行更新时才进行冲突判断
在数据表中添加一个额外列:version数据版本号或者使用时间戳timestamp,每次修改数据则版
号加1
- update tb_users set age=80,version=2 where id=5 and version=1 第一次读取数据的版本
号为1,修改时条件为version=1的那条数据。如果中间有事务已经修改了数据,则版本号绝
对不是1,所以该语句执行返回结果为0- 如果执行结果为0,则重新读取数据,重新执行修改操作
锁模式
记录锁:在行相应的索引记录上的锁,锁定一个行记录. 它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
gap锁:间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一 条索引记录之后的范围
临键锁next-key锁是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
MYSQL8 RR级别下默认使用临键锁.临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果 把事务的隔离级别降级为RC,临键锁则也会失效
意向锁:是为了支持多种粒度锁同时存在;
锁分类
- 全局锁:就对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的MDL、DDL语句、更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性
- flush tables with read lock;
- unlock table
- 行级锁
- S 共享锁 :允许获取到此锁的事务读取行
- X 排他锁 :允许获取到此锁的事务update,delete行
- 表级锁-意向锁
- 表级锁会对当前操作的整张表加锁,最常使用的 MyISAM 与 InnoDB 都支持表级锁定
- lock tables xxx read/write;
- IS 意向共享锁:事务打算对表中的行设置共享S锁
- IX 意向排他锁:事务打算对表中的行设置排他X锁
- 意向锁是 InnoDB 自动加的, 不需用户干预
- 意向锁定协议
- 事务在获得表中某行上的共享锁之前,必须先获得表上的IS锁或更高级别的锁
- 在事务可以获得表中某一行上的排他锁之前,它必须首先获得表上的IX锁
- 表级锁会对当前操作的整张表加锁,最常使用的 MyISAM 与 InnoDB 都支持表级锁定
验证,比如表aa,需要开启set AUTOCOMMIT = FALSE;
X: lock tables aa write;
S: lock tables aa read;
IX: select * from aa where 1=2 for update;
IS: select * from aa where 1=2 lock in share mode;
按加锁方式分类
按加锁方式划分,可分为自动锁、显示锁。
- 隐式加锁:
- InnoDB自动加意向锁
- 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁
- 对于普通SELECT语句,InnoDB不会加任何锁;
- 随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;锁只有在执行
commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。
- 显式加锁:
- 共享锁S:SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
- 排他锁X :SELECT * FROM table_name WHERE … FOR UPDATE
按照算法分类
按照算法分类,可分为间隙锁、临键锁、记录锁。
- 间隙锁:间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
select * from tb_users where id between 1 and 10 for update;
即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住 - 临键锁是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间,是一个左开右闭区间。临键锁的主要目的,也是为了避免幻读Phantom Read。如果把事务的隔离级别降级为RC,临键锁则也会失效。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
- 记录锁是封锁记录,记录锁也叫行锁,如 select *from tb_users where id=1 for update; 它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
相关锁总结
- 记录锁、间隙锁、临键锁,都属于排它锁
- 记录锁就是锁住一行记录
- 间隙锁只有在事务隔离级别 RR可重复读 中才会产生
- 唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁
- 普通索引不管是锁住单条,还是多条记录,都会产生间隙锁
- 间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现幻读现象
- 普通索引的间隙,优先以普通索引排序,然后再根据主键索引排序
- 事务级别是RC读已提交级别的话,间隙锁将会失效
**表级锁:**开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
**行级锁:**开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
**页面锁:**开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度
JDBC事务实现
使用JDBC连接mysql默认每一个连接是自动提交事务的。如果需要使用JDBC执行多条语句,并要求组成一个事务一起执行的话
1、在执行之前关闭自动提交,设置手动提交事务Connection的对象.setAutoCommit(false)
2、如果执行成功,手动提交事务Connection的对象.commit();
3、如果执行过程中出现异常,则手动回滚撤销操作Connection的对象.rollback();
4、补充说明:希望养成习惯,在关闭Connection的对象之前,把连接对象设置回自动提交,
Connection的对象.setAutoCommit(true)
因为实际开发中,每次获取的连接,不一定是新的连接,而是从连接池中获取的旧的连接,而且关闭也不是真关闭,而是还给连接池,供别人接着用。以防别人拿到后,以为是自动提交的,而没有commit,最终数据没有成功。
一般涉及到事务处理的话,那么业务逻辑都会比较复杂。例如购物车结算时,1)在订单表中添加一条记录。2)在订单明细表中添加多条订单明细的记录,表示该订单买了什么东西。3)修改商品表的销量和库存量。
用两条修改语句来模拟组成一个简单的事务。
- update t_department set description = ‘xx’ where did = 2;
- update t_department set description = ‘yy’ where did = 3;
希望这两个语句要么一起成功,要么一起回滚。为了制造失败,可以故意把第二条语句写错 update t_department set description = 'yy' (少了where) did = 3;
Connection conn = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///test?serverTimezone=UTC","root", "123456");
// 默认情况下单语句单事务,如果需要手动定义事务范围,则需要关系自动提交
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement("insert into t1 values(?,?)");
ps.setInt(1, 125);
ps.setString(2, "xxx5");
int len = ps.executeUpdate();
ps.setObject(1, "tttt");//数据类型错误
ps.setString(2, "66666");
ps.executeUpdate();
conn.commit();// 提交事务
} catch (Exception e) {
if (conn != null)
conn.rollback();//回滚撤销事务
System.out.println(e.getMessage());
} finally {
//如果使用直连方式,conn是否恢复原来的提交方式都没有关系;如果使用连接池则必须恢复原来的自动提交方式
if (conn != null)
conn.close();
}
分区处理
一般情况下创建的表对应一组存储文件,当数据量较大时MySQL的性能就开始下降
解决方案:如果数据表中的数据具有特定业务含义数据的特性,可以将表中数据分散到多个存储文件
中,以保证单个文件的执行效率。
最常见的分文件的方法是按照id值进行分区,不同的分区对应不同的存储问题。采用id的hash值进行分区,实际上就是对10进行取模,可以将数据均匀的分散到10个文件中
create table tb_article(
id int primary key,
title varchar(32),
content mediumtext
) partition by hash(id) partitions 10;-- 按照id的hash值进行分区,总共分为10个区
服务器端的表分区对客户端都是透明的,客户端还是照常插入数据,但是服务器端会按照设定的分区算法分散存储数据
PreparedStatement ps = conn.prepareStatement("insert into tb_article values(?,?,?)");
for (int i = 0; i < 10000; i++) {
ps.setInt(1, i+1);
ps.setString(2, i + "_name");
ps.setString(3, i+"_content");
ps.executeUpdate();
}
表分区的用途
- 逻辑数据分割
- 提高单一的写入或者读取的应用速度
- 提高分区范围查询的速度
- 分割数据能够有多个不同的物理文件路径
- 高效的保存历史数据
分区算法
MySQL支持的常见分区类型有Range、List、Hash、key分区,其中range最为常见
- Range范围:允许将数据划分到不同的范围,例如可以将一个表通过年份划分成若干个分区
- List预定义列表:允许系统通过预先定义的列表的值将数据进行分割
- hash哈希:允许通过对表中的一个或者多个列的hash key进行计算,最后通过这个hash码将数据
对应到不同的分区 - key键值:是hash分区的而一种扩展,这里的hash key是由mysql系统产生的
- 复合模式:多种模式的组合使用,例如对已经进行了range分区的表上,对其中的分区再次进行
hash分区
指定分区中的列名称时需要使用主键列或者主键中的一部分,否则设置失败
hash哈希分区
一般用于不按照业务规则进行数据文件的均匀拆分,输出的结果和输入是否有规律无关,仅适用于整形字段
create table tb_emp(
id int primary key auto_increment,
name varchar(32) not null,
hiredate date default '1989-2-3'
)partition by hash(id) partitions 4;
一般要求hash中的值最好有一定的线性关系,否则分区数据将不能均匀分布。
关键字分区key
key用于处理字符串,比hash()多一步从字符串中计算出一个整数,然后再进行取模计算
create table tb_article(
id int auto_increment,
title varchar(64) comment '文章标题',
content text,
primary key(id,title)
)partition by key(title) partitions 10;
范围分区range
range是按照一种指定数据的大小范围进行分区,例如按照文章的发布时间将数据分区存放
获取时间戳 select unix_timestamp('2022-4-30 23:59:59 ') 1651334399
select unix_timestamp('2022-3-31 23:59:59 ') 1648742399
create table tb_article(
id int auto_increment,
title varchar(32) not null,
pub_date int,
primary key(id,pub_date)
) partition by range(pub_date)(
-- 2022年3月和以前的数据
partition p202203 values less than (1648742399),
-- 2022年4月的数据
partition p202204 values less than (1651334399),
-- 2022年4月以后的数据
partition p202205 values less than maxvalue);
其中maxvalue表示最大值。MySQL允许在分区键中使用null,分区键允许是一个字段,也可以是一个表达式。一般MySQL的分区会把null当作0或者最小值进行处理。需要注意:range中null当作最小值;list中null必须出现在枚举列表中,否则不作处理;hash或者key分区中null被当作0值处理
条件运算符只能使用less than,所以要求小值在前
列表分区
列表分区也是一种条件分区,使用列表值进行分区。列表值应该是离散值,而范围分区是连续值
create table tb_article(
id int auto_increment,
title varchar(32) not null
status tinyint(1), -- 用于表示文章的状态,例如0草稿、1完成未发布、2已发布、3下架
primary key(id,status)
)partition by list(status)(
partition writing values in (1,0), -- 表示正在写的文章
partition publishing values in(2,3) -- 表示已经完成的文章
);
分区管理
range/list增加分区
针对文档数据采用年份和月份归档,随着时间的推移新增一个月份
alter table tb_article add partition(
-- 业务规则应该是小于2022-05-31 23:59:59
partition p202205 values less than (1654012799)
)
删除指定名称对应的分区
alter table tb_article drop partition p202205;
注意:删除分区后,分区中对应的数据也会随之删除
key/hash新增分区
alter table tb_article add partition paratitions 5;
销毁分区
alter table tb_article coalesce partition 6;
key和hash分区的管理不会删除数据,但是每一次调整分区,对会将所有的数据重写分配到新的分区
上。所以效率极低。一般强烈建议:要求在设计阶段就考虑好分区策略
使用分区查询
当数据表中的数据量非常大时,分区才能带来效率提升,否则性能提升不明显。
只有检索字段为分区字段时,分区带来的效率提升才能显示出来。
分区字段的选择非常重要,并且业务逻辑要尽可能的根据分区字段做相应调整。
- 指定分区查询:
select * from table partition (pname)
- mysql可以根据查询条件判断在哪个分区中,如果不能确定则查询全部
视图回顾
视图同基表一样,都是数据库对象
概述视图
MySQL视图是一种虚拟存在的表,视图也是由行列构成,但是视图并不会实际存在于数据库中,行和列的数据来自于定义视图的查询中所使用的表,并在使用视图时动态生成
视图和数据表的区别
- 视图不是真实的表,是一个虚拟表,其结构和数据都是建立对基表真实查询的基础上
- 存储在数据库中的查询操作定义了视图的内容和结构,视图的行和列的数据来自于查询所引用的实际表,引用视图时动态生成
- 视图没有实际的物理记录,数据集实际存储在基表中
- 视图是数据的窗口,基表才是真实内容。视图是查看数据表的一种方式。从安全的角度上来看,视图的数据安全性高,使用视图的开发人员不涉及数据表,甚至可以不知道基表的真实结构
- 视图的创建和删除只影响视图本身,不会影响对应的基本表
视图和基表
基表是用于存储真实数据
create table tb_dept(
id bigint primary key auto_increment,
name varchar(32)
);
-- 向基表中插入数据
insert into tb_dept(name) values('教学部'),('市场部'),('咨询部');
create table tb_emp(
id bigint primary key auto_increment,
name varchar(32) not null,
dept_id bigint not null,
foreign key(dept_id) references tb_dept(id) on delete cascade
);
insert into tb_emp(name,dept_id) values('严峻',1),('小党',3),('大展',2);
建立视图
基础语法: create view 视图名称 as 查询语句;
create view v_emp
as
select e.id empno, e.name as ename,d.name dname from tb_emp e inner join
tb_dept d on e.dept_id=d.id
视图是一个虚表,其中并不直接存储数据,但是可以当作表的方式进行使用,具体数据来源于定义视图的查询结果集
select * from v_emp;
查看视图的结构 desc 视图名称;
mysql> desc v_emp;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| empno | bigint(20) | NO | | 0 | |
| ename | varchar(32) | NO | | NULL | |
| dname | varchar(32) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.01 sec)
查看视图的定义,可以查看创建视图对应的SQL语句
show create view v_emp;
插入数据到视图【注意增删改的要求是一致的】
create view v_emp1 as select * from tb_emp where id>1;
insert into v_emp1 values (4,'熊二',1); -- 插入成功
因为向视图中插入数据实际上就是向基本表中插入数据,也就是执行 insert into v_emp1 values (4,'熊二',1);
操作就是向基本表tb_emp
中插入数据,只要没有违反基本表中的约束规则,则插入成
功。
select * from tb_emp;
mysql> insert into v_emp1 values (4,'熊二',1);
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
视图的数据来自于两个表的查询结果
mysql> desc v_emp;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| empno | bigint(20) | NO | | 0 | |
| ename | varchar(32) | NO | | NULL | |
| dname | varchar(32) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
insert into v_emp values(5,'哈哥','教学部');
ERROR 1394 (HY000): Can not insert into join view 'test.v_emp' without fields list
create view v_emp2 as select e.*,d.* from tb_emp e inner join tb_dept d on e.dept_id=d.id;
-- ERROR 1060 (42S21): Duplicate column name 'id'
create view v_emp2 as select e.*,d.id did, d.name dname from tb_emp e inner join tb_dept d on e.dept_id=d.id;
desc v_emp2;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | bigint(20) | NO | | 0 | |
| name | varchar(32) | NO | | NULL | |
| dept_id | bigint(20) | NO | | NULL | |
| did | bigint(20) | NO | | 0 | |
| dname | varchar(32) | YES | | NULL | |
+---------+-------------+------+-----+---------+-------+
5 rows in set (0.00 sec)
insert into v_emp2(id,name,dept_id,did,dname) values(6,'小哥',1,null,'研发部');
-- ERROR 1393 (HY000): Can not modify more than one base table through a join view 'test.v_emp2'
insert into v_emp2(id,name,dept_id) values(6,'小哥',1);
- 允许针对视图进行数据的修改操作,但是不允许同时修改多于一个基表
- 如果数据是通过计算得到的,也不能修改
- distinct、group by、having、union和union all
- 视图的修改实际上是针对基表数据的修改,不允许违反基本的约束规则
- 视图允许嵌套,就是
create view v_emp3 as select * from v_emp1
修改视图
当基表的结构发生变化时,可以修改视图定义。但是如果在视图定义中使用的列没有修改,则无需修改视图定义
语法: alter view 视图名称 as 新的查询语句 ;
删除视图
删除视图实际上就是删除视图的定义,不会删除数据
语法: drop view 视图名称
视图的特点
- 视图的特点:视图的列可以来自不同的表或者计算出的列,是表的抽象和在逻辑意义上建立的新关
系。 - 视图是由基本表(实表)产生的表(虚表)。视图的建立和删除不影响基本表。
- 对视图内容的更新(添加,删除和修改)直接影响基本表。
- 当视图来自多个基本表时,有时可以修改数据,有时不允许修改数据。
- 不能同时操作两个表
- 不能破坏基本规则
- 视图的操作包括创建视图,查看视图,删除视图和修改视图。
优点
1、可以从基表中进行数据定制,简化数据操作,提高数据的安全性
2、共享所需数据、修改数据格式
3、重用sql语句
库表切分
使用分区可以将数据文件的变小,在一定程度上提高查询效率,但是业务区分很困难
使用视图可以实现按需获取数据,但是所查询的数据并没有变化,所以并不能提高查询效率
引入切分的原因:
- 为数据库减压
- 分区算法的局限性
- 针对分区算法只有MySQL5.1+之后才支持
水平拆分
水平分割:通过建立结构相同的几张表分别存储数据 类似分区
能够把一个特别大的表,水平拆分到不同的数据库服务器上,来分担一个数据库服务器的压力。提高数据库的性能和效率。
优点:
- 单表的并发能力提高了,磁盘 I/O 性能也提高了。
- 如果出现高并发的话,总表可以根据不同的查询,将并发压力分到不同的小表里面。
缺点:
- 无法实现表连接查询
select * from test2.t_emp e,test1.t_dept d where e.dept_id=d.id; 在一台设备上两个数据
库之间连接查询还好,但是如果test1和test2分别位于不同设备上,则查询出问题
垂直拆分
垂直分割:将经常一起使用的字段放在一个单独的表中,其余字段存放在另外的表中,分割后的表记录之间是一对一的关系。
可以避免所有的业务表全部放在一台MySQL数据库服务器上,通过增加MySQL数据库实例的数,来分担来自业务层模块的数据请求压力,从而达到对降低MySQL数据库的压力。
优点:
- 减少增量数据写入时的锁对查询的影响。
- 由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘 IO,时延变短。
缺点:
- 无法解决单表数据量太大的问题。
典型案例:
模式:学生(学生编号,姓名、性别、住址、大字段属性用于存放照片)
解决方案
create table tb_student(
id bigint primary key auto_increment,
name varchar(32) not null,
sex boolean default 1
) comment '存放学生基础信息';
create table tb_student_image(
id bigint primary key,
foreign key(id) references tb_student(id) on delete cascade, -- 实现一对一关联
photo longblob -- 如果表中有大对象字段,则一定需要进行分表处理
)comment '学生的扩展信息';
id重复的解决方案
水平切分时要求id不能重复,常见的解决方案有2种:1、单独创建一个表,其中只包含一个id字段,每
次自增该字段作为插入数据的id使用。2、可以借用第三方应用,例如memcache或者redis的id自增实
现
集群
在一些大型网站业务场景中,单台数据库服务器所能提供的并发量已经无法满足业务需求,为了满足这种情况,一般而言是通过主从同步的方式来同步数据,在此基础上,通过读写分离来提升数据库的并发和负载能力。
一般可以采用横向扩展和库表切分的方法实现数据库处理能力的提升,因为单机的硬件处理能力一定是有上限。由此而生的相关技术有:读写分离和负载均衡
MySQL的主从复制
需要实现集群多台机器共同对外提供服务,需要考虑的是如何实现读操作和写操作的工作划分。首先就需要部署主从复制,只有实现了主从复制,才能在此基础上实现读写分离
MySQL所支持的复制类型
- 基于语句的复制,MySQL默认采用的是基于语句的复制,效率比较高
- 基于行的复制,把改变的内容复制过去,而不是把命令复制到从服务器上再次执行一次
- 混合类型复制,默认采用基于语句的复制,一旦发现基于语句无法精确复制时,就会采用基于行的复制
主从复制的工作过程[重点]
master负责完成写操作,slave负责执行复制操作
1、在每个事务更新数据完成之前,master主机在二进制日志种记录这些改变binlog。再写入二进制日
志完成后,master通知存储引擎提交事务
2、从机slave将master的binlog复制到其中的中继日志relaylog。首先slave启动一个工作线程IO线程,
IO线程在master上打开一个普通的连接,然后开始binlog dump process。binlog dump process从
master的二进制日志种读取操作事件,如果已经跟上了master,它会休眠并等待master产生新的事
件。IO线程将事件写入中继日志relay log
3、SQL从线程从中继日志读取事件,重放其中的事件而更新slave种的数据,使slave种的数据和master种的一致
注意:master的并行更新在slave种是串行执行的
主从节点配置
一般常见的主从集群会采用3个节点构成,1主多从
实验方式2种:采用centos+虚拟机、windows下通过不同的端口配置1主1从
windows下采用解压版,防止在多次安装的时候告诉已安装mysql不允许再次安装
master配置
端口号使用3306
mysql的安装路径下,名称为my.ini
[mysqld]
# 服务器的编号值,各个服务器的编号不能重复
server-id=6
# 开启二进制日志
log-bin=mysql-bin
# 设置日志的过期时间,避免占满磁盘
expire-logs-days=7
# binlog-ignore-db不使用主从复制的数据库 binlog-do-db可以设置需要进行主从复制的数据库
binlog-ignore-db=mysql
# 设置3306端口
port=3306
# mysql服务器具体的安装路径
basedir=D:/software/mysql-8.0.15-master
# mysql的本地数据文件的存储路径
datadir=D:/software/mysql-8.0.15-master/data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数
max_connect_errors=10
# 默认编码字符集
character-set-server=utf8mb4
# 默认存储引擎
default-storage-engine=INNODB
# 默认使用mysql_native_password插件进行口令认证
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8mb4
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8mb4
初始化操作 mysqld --initialize --console
将mysql添加到系统服务中 mysqld --install mymaster
启动master节点的数据库服务 net start mymaster
使用mysql客户端连接数据库修改root的默认口令``
slave配置
mysql的安装路径下,名称为my.ini
[mysqld]
server-id=16
# 设置3316端口
port=3316
# mysql服务器具体的安装路径
basedir=D:/software/mysql-8.0.15-slave
# mysql的本地数据文件的存储路径
datadir=D:/software/mysql-8.0.15-slave/data
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3316
default-character-set=utf8mb4
初始化新的mysql数据库系统 mysqld --initialize --console
安装系统服务 mysqld --install myslave
启动数据库 net start myslave
修改默认口令
查看主库状态show master status;
主要需要查看记录二进制日志具体名称File和对应的偏移量
Posititon
mysql> show master status;
+------------------+----------+--------------+------------------+----------------
---+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
Executed_Gtid_Set |
+------------------+----------+--------------+------------------+----------------
---+
| mysql-bin.000002 | 445 | | mysql |
|
+------------------+----------+--------------+------------------+----------------
---+
1 row in set (0.00 sec)
针对从库设置主库的配置信息
change master to master_host='localhost', master_port=3306, master_user='root',
master_password='123456', master_log_file='mysql-bin.000002',master_log_pos=445;
- master_host:主库ip
- master_port:主库端口号
- master_user:主库账号
- master_password:主库密码
- master_log_file:对应主库状态中的file
- master_log_pos:对应主库状态中的position
启动从库 start slave;
查看从库状态 show slave status;
主库进行插入修改数据的时候从库就可以自动进行复制
MySQL读写分离
读写分离实际上就是在主从复制的基础上,只在主服务器上执行写操作,只在从服务器上执行读操作。基本原理就是让主数据库处理事务性操作,而从数据库处理select查询,数据库的主从复制用来将事务性操作导致的数据变更同步到集群种的从数据库上
典型的实现方式就是基于中间代理层的实现:代理一般位于客户端和服务器之间,代理服务器接收到客户端请求后通过判断所执行的操作后转发到对应的后端数据库服务器上
读写分离的原因
数据库写入效率要低于读取效率,一般系统中数据读取频率高于写入频率,单个数据库实例在写入的时候会影响读取性能,这是做读写分离的原因。
主从复制的原理
首先启动master,然后启动slave,
在master中执行命令 show master status;
,File表示实现复制功能的日志,就是二进制日志binlog。
Position表示二进制日志文件的偏移量,偏移量之后的操作都会同步到slave,在偏移量之前的则需要收到导入
mysql> show master status;
+------------------+----------+--------------+------------------+----------------
---+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
Executed_Gtid_Set |
+------------------+----------+--------------+------------------+----------------
---+
| mysql-bin.000002 | 445 | | mysql |
|
+------------------+----------+--------------+------------------+----------------
---+
1 row in set (0.00 sec)
针对从库设置主库的配置信息。这里的master_log_file和master_log_pos都来自于master中查询结果
change master to master_host='localhost', master_port=3306, master_user='root',
master_password='123456', master_log_file='mysql-bin.000002',master_log_pos=445;
mysql> show master status;
+------------------+----------+--------------+------------------+----------------
---+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
Executed_Gtid_Set |
+------------------+----------+--------------+------------------+----------------
---+
| mysql-bin.000002 | 445 | | mysql |
|
+------------------+----------+--------------+------------------+----------------
---+
1 row in set (0.00 sec)
master上面的任何修改操作都会记录到二进制日志中,slave上会启动一个IO线程,连接到master上请
求读取二进制日志文件,注意这里会有一定的延迟。slave将读取到的二进制日志数据写入到本地的中继日志relay log中。slave会开启一个SQL线程,定时检查relay log的修改,如果发现有更改的内容,则在本机上执行一次修改操作。这样就是实现了将master上的修改同步到slave上的目的
如果是一主多从时,主库既要负责写又要负责为几个从库提供二进制日志,工作压力有点大。所以在一般的生产环境后可以稍作调整,将二进制日志只发送给某个从节点。这个从节点再开启二进制日志,并将自己的二进制日志再次发送给其它从节点,或者这个从节点不记录,只是负责将二进制日志转发给其它从节点。这样的结构性能会比简单的一主多从的模式性能好很多,而且数据之间的延迟应该会稍好一些。
一主会有单点故障问题,解决方案为主备模式的主主复制
读写分离实现
Mysql读写分离可以基于第三方插件,例如Mycat或者基于程序读写分离(应用内部路由)
应用内路由
基于spring的AOP实现。
用aop来拦截spring项目的dao层方法,根据方法名称就可以判断要执行的sql类型(即是read还是write类型),进而动态切换主从数据源。
基于jdbc驱动方式
使用mysql驱动mysql-connector-java的Connector/J的可以实现读写分离。即在jdbc的url中配置
jdbc:mysql:replication://master,slave1,slave2,slave3/test
java程序通过在连接MySQL的jdbc中配置主库与从库等地址,jdbc会自动将读请求发送给从库,将写请求发送给主库,此外,mysql的jdbc驱动还能够实现多个从库的负载均衡。
Mycat中间件方式
Mycat可以实现对业务数据库的分库分表、读写分离,能够很方便的构建一个以Mycat为核心的数据库集群架构,以企业级方案解决数据库出现的性能问题
Mycat能够将mysql数据组成一个分布式的集群,可以把物理上多个独立的mysql数据库,搞成一个逻辑上的整体,使开发者感受不到的物理上的独立。这些物理上独立的mysql数据库按照配置不同各自完成各自的工作。
数据库优化
需要考虑优化的场景
- 系统的吞吐量一般出现在数据库的访问速度上
- 随着数据量的逐步增大,处理时间会相应变慢
- 数据存储在硬盘上,读写速度和内存不匹配
优化方案
- 硬件优化
- 缓存优化
- 设计优化
- sql语句优化
sql性能分析profile
利用mysql中的profile可以记录所有的SQL执行的详细信息
开启profile
-- 查看环境变量
show variables like 'profiling';
mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | OFF |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
-- 开启profile日志
set profiling=on;
使用profile
insert into tb_emp values(8,'东方',2);
-- 查看profile记录
show profiles;
mysql> show profiles;
+----------+------------+---------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------+
| 1 | 0.00312925 | show variables like 'profiling' |
| 2 | 0.00190150 | SELECT DATABASE() |
| 3 | 0.01369875 | show tables |
| 4 | 0.00511900 | select * from tb_emp |
| 5 | 0.01092400 | insert into tb_emp values(8,'????',2) |
+----------+------------+---------------------------------------+
5 rows in set, 1 warning (0.00 sec)
查看特定查询的统计信息
show profile for query 查询编号Query_Id
。可以查看执行该SQL经过了那些步骤,每个步骤所消耗的时间
mysql> show profile for query 4;
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000050 |
| Executing hook on transaction | 0.000003 |
| starting | 0.000005 |
| checking permissions | 0.000003 |
| Opening tables | 0.004257 |
| init | 0.000011 |
| System lock | 0.000007 |
| optimizing | 0.000002 |
| statistics | 0.000010 |
| preparing | 0.000008 |
| executing | 0.000002 |
| Sending data | 0.000608 |
| end | 0.000005 |
| query end | 0.000002 |
| waiting for handler commit | 0.000005 |
| closing tables | 0.000006 |
| freeing items | 0.000127 |
| cleaning up | 0.000011 |
+--------------------------------+----------+
18 rows in set, 1 warning (0.00 sec)
慢查询日志
MySQL慢查询全名称为慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中执行sql语句的响应时间超过阈值的sql语句
- 具体环境中,运行时间超过long_query_time值的sql语句会被记录在慢查询日中。
long_query_time的默认值为10,意思是记录运行时间为10秒以上的sql语句 - 默认情况下,MySQL并不会开启慢查询日志,需要手工设置这个参数。如果不是进行系统调优的需要时,不建议启动该参数。因为启动慢查询日志会带来一定的性能损耗
- 慢查询日志支持将日志信息记录在文件或者数据表。用于记录执行时间超过临界阈值的sql语句,
可以快速定位应用中的慢查询,是sql语句优化的参考和基准
开启慢查询
配置项slow_query_log
查看慢查询是否开启 show variables like 'slow_query_log';
,环境变量值为OFF表示关闭
mysql> show variables like 'slow_query_log';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | OFF |
+----------------+-------+
1 row in set, 1 warning (0.00 sec)
启动慢查询 set global slow_query_log=on
开启后在datadir下则会产生一个xxx-slow.log文件,其中存储慢查询日志
设置临界时间
MySQL默认慢查询的阈值为10秒,允许通过配置参数long_query_time进行修改,单位为秒
查看慢查询阈值: show variables like 'long_query_time';
mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set, 1 warning (0.00 sec)
修改默认阈值set long_query_time=1;
将阈值修改为1s
执行一个长时间的查询操作 select sleep(5);
mysql> select sleep(5);
+----------+
| sleep(5) |
+----------+
| 0 |
+----------+
1 row in set (5.00 sec)
具体的日志记录
D:\software\mysql-8.0.15-winx64\bin\mysqld, Version: 8.0.15 (MySQL Community
Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time Id Command Argument
# Time: 2022-05-08T03:46:17.632649Z 这个时间默认采用的是UTC时间,所以有8小时时差
# User@Host: root[root] @ localhost [::1] Id: 8
# Query_time: 5.001430 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
use test;
SET timestamp=1651981572;
select sleep(5);
慢查询的相关配置参数
- slow_query_log是否开启慢查询日志,1开启0关闭
- slow_query_log_file设置慢查询日志的存储路径,可以不设置该参数,系统会默认一个缺省文件
【主机名称-slow.log】 - long_query_time设置慢查询的阈值,默认为10秒钟,当查询时间多于设置的阈值时,可以记录日
志 - log_queries_not_using_indexes设置没有使用索引的查询会被记录到查询日志中
- log_output设置日志存储方式,值为FILE表示采用文件的方式记录日志,默认值FILE;如果值为
TABLE表示将慢查询日志记录存储在数据库
在命令行中执行set global show_query_log=1
可以开启慢查询日志,但是仅仅只对当前数据库生效,如果MySQL重启后则会失效。如果需要永久生效,就必须修改my.ini配置文件
注意:实际上的慢查询的阈值设置需要考虑具体情况,一般从大到小逐步筛查,目的在于将最慢的SQL语句优化掉
设计优化
- 设计数据库时,应该充分考虑数据库表和字段的设计以及存储引擎的选择
1、尽量使用整型数表示字符串。例如存储IP地址:inet_aton(字符串)和inet_ntoa(数值)
2、表的字段类型一般不采用enum和set类型,因为维护成本太高,可以采用关联表的方式来
替代enum
3、使用decimal可以对浮点数进行精确存放,但是存储空间会随着数值的增大而增大;一般
建议使用固定空间,例如double,但是double会损失存储精度。
4、尽可能使用not null约束,针对允许为null的字段可以考虑定义default。由于null值判断
比较繁琐。例如不要使用 id int ,可以考虑使用 id int default 0
5、字段注释需完整,最好可以见名知意
6、一般建议单表的字段不易过多。一般20-30就是极限
7、可以有预留字段。
总之所有的设计过程就是在性能和需求之间平衡的结果
- 充分利用MySQL自身提供的功能,例如索引等
需要记忆NF3和反范式
需要记忆常见的索引失效情形和执行计划查询的命令 exaplain
- 横向扩展,引入MySQL集群、负载均衡和读写分离
- SQL语句优化
1、使用limit对查询结果的记录进行限定
2、避免使用 select * ,将需要查询的字段列表出来 【原因】
3、使用join代替子查询
4、拆分大的delete或者insert语句【delete和truncate table】
5、可以通过开启慢查询定位应用中的执行较慢的sql语句
6、一般不进行列计算。例如 select id from t_users where age+1=10; 但是针对列的计
算操作会导致整表扫描,一般建议查询时尽可能将操作移动到等号的右边 select if from
t_users where age=10-1
7、sql语句尽可能简单,因为一个sql语句只能在一个CPU中进行计算,将大语句拆分为小语
句,可以减少锁定时间,避免出现一个大sql语句锁定整个库的访问
8、or尽可能修改为in,因为or的效率为O(n),而in的效率是O(logN)。但是in的个数建议控
制在200以内
9、避免使用%xxx样式的查询
10、尽可能使用同类型数据进行比较,例如 ‘123’=123
11、尽量避免在where子句中使用!=操作符,因为可能会出现使用全表扫描
12、对于连续值使用between/and,不使用in
压力测试mysqlslap
安装mysql时,系统自带了一个压力测试工具mysqlslap,位于bin目录下
1、自动生成sql测试
mysqlslap --auto-generate-sql -uroot -p123456
D:\software\mysql-8.0.15-winx64\bin>mysqlslap --auto-generate-sql -uroot -
p123456
mysqlslap: [Warning] Using a password on the command line interface can be
insecure.
Benchmark
Average number of seconds to run all queries: 0.031 seconds
Minimum number of seconds to run all queries: 0.031 seconds
Maximum number of seconds to run all queries: 0.031 seconds
Number of clients running queries: 1
Average number of queries per client: 0
2、并发测试,例如默认100个并发量
mysqlslap --auto_generate-sql -uroot -p123456 --concurrency=100
3、多轮测试,循环测试10次
mysqlslap --auto-generate-sql -uroot -p123456 --concurrency=100 --iterations=10
语句优化步骤
1、分析使用慢查询日志,查找需要进行优化的SQL语句。
2、针对慢查询的常见优化方法
- 创建合理的索引,并使用explain查看执行计划,确认索引是否生效
- 优化数据库结构。在设计过程中需要考虑数据冗余、查询和更新的速度、字段的数据类型是
否合理等方面。- 分解关联查询,可以将一个大的复杂查询分为多个小查询,可以对每个需要关联的表单独查
询,然后将查询结果在应用程序中进行关联- 典型的考试题:针对limit分页的优化查询 select * from t_users limit 1000000,10 ;
- 分析具体的SQL语句,例如选择数据量小的文件充当驱动表,将in子查询转换为exists子查询
MySQL与Oracle
1、并发性
并发性是OLTP数据库最重要的特性,但并发涉及到资源的获取、共享与锁定。
- mysql:mysql以表级锁为主,对资源锁定的粒度很大,如果一个session对一个表加锁时间过长,
会让其他session无法更新此表中的数据。虽然InnoDB引擎的表可以用行级锁,但这个行级锁的机制依赖于表的索引,如果表sql语句没有使用索引,那么仍然使用表级锁。 - oracle:oracle使用行级锁,对资源锁定的粒度要小很多,只是锁定sql需要的资源,并且加锁是在数据库中的数据行上,不依赖与索引。所以oracle对并发性的支持要好很多。
2、事务
- oracle很早就完全支持事务。
- mysql在innodb存储引擎的行级锁的情况下才支持事务。
3、数据持久性 - oracle:保证提交的数据均可恢复,因为oracle把提交的sql操作线写入了在线联机日志文件中,保持到了磁盘上,如果出现数据库或主机异常重启,重启后oracle可以考联机在线日志恢复客户提交的数据。
- mysql:默认提交sql语句,但如果更新过程中出现db或主机重启的问题,也许会丢失数据。
4、提交方式
- oracle默认不自动提交,需要用户手动提交。
- mysql默认是自动提交。
5、逻辑备份
- oracle逻辑备份时不锁定数据,且备份的数据是一致的。
- mysql逻辑备份时要锁定数据,才能保证备份的数据是一致的,影响业务正常的dml使用。
6、热备份
- oracle有成熟的热备工具rman,热备时,不影响用户使用数据库。即使备份的数据库不一致,也
可以在恢复时通过归档日志和联机重做日志进行一致的回复。 - mysql:
- myisam的引擎,用mysql自带的mysqlhostcopy热备时,需要给表加读锁,影响dml操作
- innodb的引擎,它会备份innodb的表和索引,但是不会备份.frm文件。用ibbackup备份时,
会有一个日志文件记录备份期间的数据变化,因此可以不用锁表,不影响其他用户使用数据
库。但此工具是收费的。
innobackup是结合ibbackup使用的一个脚本,他会协助对.frm文件的备份。
7、sql语句的扩展和灵活性
- mysql对sql语句有很多非常实用而方便的扩展,比如limit功能,insert可以一次插入多行数据,
select某些管理数据可以不加from。 - oracle在这方面感觉更加稳重传统一些。
8、复制
- oracle:既有推或拉式的传统数据复制,也有dataguard的双机或多机容灾机制,主库出现问题
是,可以自动切换备库到主库,但配置管理较复杂 - mysql:复制服务器配置简单,但主库出问题时,丛库有可能丢失一定的数据。且需要手工切换丛库到主库。
9、性能诊断
- oracle有各种成熟的性能诊断调优工具,能实现很多自动分析、诊断功能。比如awr、addm、
sqltrace、tkproof等 - mysql的诊断调优方法较少,主要有慢查询日志。
10、权限与安全
- mysql的用户与主机有关,感觉没有什么意义,另外更容易被仿冒主机及ip有可乘之机。
- oracle的权限与安全概念比较传统,中规中矩。
11、管理工具
- oracle有多种成熟的命令行、图形界面、web管理工具,还有很多第三方的管理工具,管理极其方
便高效。 - mysql管理工具较少,在linux下的管理工具的安装有时要安装额外的包,有一定复杂性。
容灾和日志
用mysqldump工具对innodb存储引擎的数据库做完全热备,并且滚动二进制日志,为了下次恢复或者
增量方便,还要记录一下当前二进制日志文件位置
mysqldump -uroot -p123456 --all-databases > /root/mylogs/mydb_all.sql
mysql -uroot -p123456 < mydb_all.sql
--databases \\指定数据库名
--all-databases \\备份服务器上的所有库