MySQL主要分为 Server 层和存储引擎层:
- Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
- 存储引擎: 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。
Server 层基本组件
- 连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。
- 查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个 sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。
- 优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。
- 执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。
分库分表
当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。
数据切分可以分为两种方式:垂直划分和水平划分。
垂直划分
垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。
优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。
缺点:
- 主键出现冗余,需要管理冗余列;
- 会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;
- 依然存在单表数据量过大的问题。
水平划分
水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。
优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。
缺点:
- 分片事务一致性难以解决
- 跨节点
join
性能差,逻辑复杂 - 数据分片在扩容时需要迁移
什么是分区表?
分区是把一张表的数据分成N多个区块。分区表是一个独立的逻辑表,但是底层由多个物理子表组成。
当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。
分区一般都是放在单机里的,用的比较多的是时间范围分区,方便归档。只不过分库分表需要代码实现,分区则是mysql内部实现。分库分表和分区并不冲突,可以结合使用。
分区表类型
range分区,按照范围分区。比如按照时间范围分区
CREATE TABLE test_range_partition( id INT auto_increment, createdate DATETIME, primary key (id,createdate) ) PARTITION BY RANGE (TO_DAYS(createdate) ) ( PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ), PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ), PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ), PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ), PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ), PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ), PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ), PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ), PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ), PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ), PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ), PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') ) );
在/var/lib/mysql/data/
可以找到对应的数据文件,每个分区表都有一个使用#分隔命名的表文件:
-rw-r----- 1 MySQL MySQL 65 Mar 14 21:47 db.opt -rw-r----- 1 MySQL MySQL 8598 Mar 14 21:50 test_range_partition.frm -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201801.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201802.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201803.ibd ...
list分区
list分区和range分区相似,主要区别在于list是枚举值列表的集合,range是连续的区间值的集合。对于list分区,分区字段必须是已知的,如果插入的字段不在分区时的枚举值中,将无法插入。
create table test_list_partiotion ( id int auto_increment, data_type tinyint, primary key(id,data_type) )partition by list(data_type) ( partition p0 values in (0,1,2,3,4,5,6), partition p1 values in (7,8,9,10,11,12), partition p2 values in (13,14,15,16,17) );
hash分区
可以将数据均匀地分布到预先定义的分区中。
create table test_hash_partiotion ( id int auto_increment, create_date datetime, primary key(id,create_date) )partition by hash(year(create_date)) partitions 10;
分区的问题?
- 打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL 需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、
LOAD DATA INFILE
和一次删除多行数据。 - 维护分区的成本可能很高。例如重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。
- 所有分区必须使用相同的存储引擎。
查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。
举个例子,查询语句如下:
select * from user where id > 1 and name = '大彬';
- 首先检查权限,没有权限则返回错误;
- MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;
- 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;
- 两种执行方案,先查
id > 1
还是name = '大彬'
,优化器根据自己的优化算法选择执行效率最好的方案; - 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。
更新语句执行过程?
更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo log
(prepare
状态)、binlog
、redo log
(commit
状态)
举个例子,更新语句如下:
update user set name = '大彬' where id = 1;
- 先查询到 id 为1的记录,有缓存会使用缓存。
- 拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录
redo log
,此时redo log
进入prepare
状态。 - 执行器收到通知后记录
binlog
,然后调用引擎接口,提交redo log
为commit
状态。 - 更新完成。
为什么记录完redo log
,不直接提交,而是先进入prepare
状态?
假设先写redo log
直接提交,然后写binlog
,写完redo log
后,机器挂了,binlog
日志没有被写入,那么机器重启后,这台机器会通过redo log
恢复数据,但是这个时候binlog
并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
exist和in的区别?
exists
用于对外表记录做筛选。exists
会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists
里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists
里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。
exists
用于对外表记录做筛选。exists
会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists
里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists
里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。
select a.* from A awhere exists(select 1 from B b where a.id=b.id)
in
是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。
select * from Awhere id in(select id from B)
子查询的表比较大的时候,使用exists
可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in
可以有效减少对外查询表循环遍历来提升速度。
MySQL中int(10)和char(10)的区别?
int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。
truncate、delete与drop区别?
相同点:
-
truncate
和不带where
子句的delete
、以及drop
都会删除表内的数据。 -
drop
、truncate
都是DDL
语句(数据定义语言),执行后会自动提交。
不同点:
- truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;
- 一般来说,执行速度: drop > truncate > delete。
having和where区别?
- 二者作用的对象不同,
where
子句作用于表和视图,having
作用于组。 where
在数据分组前进行过滤,having
在数据分组后进行过滤。
为什么要做主从同步?
- 读写分离,使数据库能支撑更大的并发。
- 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。
- 数据备份,保证数据的安全。
什么是MySQL主从同步?
主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master
),其余的服务器充当从服务器(slave
)。
因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。
数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。
- 悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加
version
字段,在修改提交之前检查version
与原来取到的version
值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS
算法实现。
用过processlist吗?
用过processlist吗?
show processlist
或 show full processlist
可以查看当前 MySQL 是否有压力,正在运行的SQL
,有没有慢SQL
正在执行。返回参数如下:
- id:线程ID,可以用
kill id
杀死某个线程 - db:数据库名称
- user:数据库用户
- host:数据库实例的IP
- command:当前执行的命令,比如
Sleep
,Query
,Connect
等 - time:消耗时间,单位秒
- state:执行状态,主要有以下状态:
- Sleep,线程正在等待客户端发送新的请求
- Locked,线程正在等待锁
- Sending data,正在处理
SELECT
查询的记录,同时把结果发送给客户端 - Kill,正在执行
kill
语句,杀死指定线程 - Connect,一个从节点连上了主节点
- Quit,线程正在退出
- Sorting for group,正在为
GROUP BY
做排序 - Sorting for order,正在为
ORDER BY
做排序
- info:正在执行的
SQL
语句
MySQL查询 limit 1000,10 和limit 10 速度一样快吗?
两种查询方式。对应 limit offset, size
和 limit size
两种方式。
而其实 limit size
,相当于 limit 0, size
。也就是从0开始取size条数据。
也就是说,两种方式的区别在于offset是否为0。
先来看下limit sql的内部执行逻辑。
MySQL内部分为server层和存储引擎层。一般情况下存储引擎都用innodb。
server层有很多模块,其中需要关注的是执行器是用于跟存储引擎打交道的组件。
执行器可以通过调用存储引擎提供的接口,将一行行数据取出,当这些数据完全符合要求(比如满足其他where条件),则会放到结果集中,最后返回给调用mysql的客户端。
以主键索引的limit执行过程为例:
执行select * from xxx order by id limit 0, 10;
,select后面带的是星号,也就是要求获得行数据的所有字段信息。
server层会调用innodb的接口,在innodb里的主键索引中获取到第0到10条完整行数据,依次返回给server层,并放到server层的结果集中,返回给客户端。
把offset搞大点,比如执行的是:select * from xxx order by id limit 500000, 10;
server层会调用innodb的接口,由于这次的offset=500000,会在innodb里的主键索引中获取到第0到(500000 + 10)条完整行数据,返回给server层之后根据offset的值挨个抛弃,最后只留下最后面的size条,也就是10条数据,放到server层的结果集中,返回给客户端。
可以看出,当offset非0时,server层会从引擎层获取到很多无用的数据,而获取的这些无用数据都是要耗时的。
因此,mysql查询中 limit 1000,10 会比 limit 10 更慢。原因是 limit 1000,10 会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。
高度为3的B+树,可以存放多少数据?
InnoDB存储引擎有自己的最小储存单元——页(Page)。
查询InnoDB页大小的命令如下:
mysql> show global status like 'innodb_page_size'; +------------------+-------+ | Variable_name | Value | +------------------+-------+ | Innodb_page_size | 16384 | +------------------+-------+
可以看出 innodb 默认的一页大小为 16384B = 16384/1024 = 16kb。
在MySQL中,B+树一个节点的大小设为一页或页的倍数最为合适。因为如果一个节点的大小 < 1页,那么读取这个节点的时候其实读取的还是一页,这样就造成了资源的浪费。
B+树中非叶子节点存的是key + 指针;叶子节点存的是数据行。
对于叶子节点,如果一行数据大小为1k,那么一页就能存16条数据。
对于非叶子节点,如果key使用的是bigint,则为8字节,指针在MySQL中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170 个索引指针。
于是可以算出,对于一颗高度为2的B+树,根节点存储索引指针节点,那么它有1170个叶子节点存储数据,每个叶子节点可以存储16条数据,一共 1170 x 16 = 18720 条数据。而对于高度为3的B+树,就可以存放 1170 x 1170 x 16 = 21902400 条数据(两千多万条数据),也就是对于两千多万条的数据,我们只需要高度为3的B+树就可以完成,通过主键查询只需要3次IO操作就能查到对应数据。
所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。
深分页怎么优化?
还是以上面的SQL为空:select * from xxx order by id limit 500000, 10;
方法一:
从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,拷贝完整数据相比只拷贝行数据里的其中一两个列字段更耗费时间。
因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成:
select * from xxx where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10;
先执行子查询 select id from xxx by id limit 500000, 1
, 这个操作,其实也是将在innodb中的主键索引中获取到500000+1
条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。
但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。
在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了
select * from xxx where id >=500000 order by id limit 10;
这样innodb再走一次主键索引,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。
方法二:
将所有的数据根据id主键进行排序,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。
select * from xxx where id > start_id order by id limit 10;
通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。
大表查询慢怎么优化?
某个表有近千万数据,查询比较慢,如何优化?
当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:
- 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描
- 索引优化,SQL优化。最左匹配原则等,参考:https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95
- 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能
- 利用缓存。利用Redis等缓存热点数据,提高查询效率
- 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内
- 读写分离。经典的数据库拆分方案,主库负责写,从库负责读
- 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分
- 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDERBY命令上涉及的列建立索引
- 数据异构到es
- 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中
- 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、tidb)
MySQL单表多大进行分库分表?
目前主流的有两种说法:
- MySQL 单表数据量大于 2000 万行,性能会明显下降,考虑进行分库分表。
- 阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为MySQL为了提高性能,会将表的索引装载到内存中。在InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。
因此,对于分库分表,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
至于MySQL单表多大进行分库分表,应当根据机器资源进行评估。
说说count(1)、count(*)和count(字段名)的区别
嗯,先说说count(1) and count(字段名)的区别。
两者的主要区别是
- count(1) 会统计表中的所有的记录数,包含字段为null 的记录。
- count(字段名) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。
接下来看看三者之间的区别。
执行效果上:
- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
- count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
- count(字段名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。
执行效率上:
- 列名为主键,count(字段名)会比count(1)快
- 列名不为主键,count(1)会比count(列名)快
- 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
- 如果有主键,则 select count(主键)的执行效率是最优的
- 如果表只有一个字段,则 select count(*)最优。
MySQL中DATETIME 和 TIMESTAMP有什么区别?
嗯,TIMESTAMP
和DATETIME
都可以用来存储时间,它们主要有以下区别:
1.表示范围
- DATETIME:1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999
- TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-09 03:14:07.999999' UTC
TIMESTAMP
支持的时间范围比DATATIME
要小,容易出现超出的情况。
2.空间占用
- TIMESTAMP :占 4 个字节
- DATETIME:在 MySQL 5.6.4 之前,占 8 个字节 ,之后版本,占 5 个字节
3.存入时间是否会自动转换
TIMESTAMP
类型在默认情况下,insert、update 数据时,TIMESTAMP
列会自动以当前时间(CURRENT_TIMESTAMP
)填充/更新。DATETIME
则不会做任何转换,也不会检测时区,你给什么数据,它存什么数据。
4.TIMESTAMP
比较受时区timezone的影响以及MYSQL版本和服务器的SQL MODE的影响。因为TIMESTAMP
存的是时间戳,在不同的时区得出的时间不一致。
5.如果存进NULL,两者实际存储的值不同。
- TIMESTAMP:会自动存储当前时间 now() 。
- DATETIME:不会自动存储当前时间,会直接存入 NULL 值。
说说为什么不建议用外键?
外键是一种约束,这个约束的存在,会保证表间数据的关系始终完整。外键的存在,并非全然没有优点。
外键可以保证数据的完整性和一致性,级联操作方便。而且使用外键可以将数据完整性判断托付给了数据库完成,减少了程序的代码量。
虽然外键能够保证数据的完整性,但是会给系统带来很多缺陷。
1、并发问题。在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。
2、扩展性问题。比如从MySQL
迁移到Oracle
,外键依赖于数据库本身的特性,做迁移可能不方便。
3、不利于分库分表。在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。
使用自增主键有什么好处?
自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。
InnoDB的自增值为什么不能回收利用?
主要为了提升插入数据的效率和并行度。
假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。
假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。
事务 B 正确提交了,但事务 A 出现了唯一键冲突。
如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。
接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。
而为了解决这个主键冲突,有两种方法:
- 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
- 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。
可见,这两个方法都会导致性能问题。
因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。
自增主键保存在什么地方?
不同的引擎对于自增值的保存策略不同:
- MyISAM引擎的自增值保存在数据文件中。
- 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。
自增主键一定是连续的吗?
不一定,有几种情况会导致自增主键不连续。
1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。
2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。
3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:
- 语句执行过程中,第一次申请自增id,分配1个自增id
- 1个用完以后,第二次申请,会分配2个自增id
- 2个用完以后,第三次申请,会分配4个自增id
- 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)
如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。
4、自增步长不是1,也会导致自增主键不连续。
MySQL数据如何同步到Redis缓存?
有两种方案:
1、通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现。
过程大致如下:
- 在MySQL中对要操作的数据设置触发器Trigger,监听操作
- 客户端向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
- UDF函数可以把数据写入到Redis中,从而达到同步的效果
2、解析MySQL的binlog,实现将数据库中的数据同步到Redis。可以通过canal实现。canal是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费。
canal的原理如下:
- canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
- mysql master收到dump请求,开始推送binary log给canal
- canal解析binary log对象(原始为byte流),将数据同步写入Redis。
为什么阿里Java手册禁止使用存储过程?
先看看什么是存储过程。
存储过程是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。
存储过程主要有以下几个缺点。
- 存储过程难以调试。存储过程的开发一直缺少有效的 IDE 环境。SQL 本身经常很长,调试式要把句子拆开分别独立执行,非常麻烦。
- 移植性差。存储过程的移植困难,一般业务系统总会不可避免地用到数据库独有的特性和语法,更换数据库时这部分代码就需要重写,成本较高。
- 管理困难。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。
- 存储过程是只优化一次,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。