MySql

索引

  1. MySQL 官方对索引的定义为:索引(Index)是帮助 MySQL 高效获取数据的数据结构。
  2. 索引是满⾜某种特定查找算法的数据结构,⽽这些数据结构会以某种⽅式指向数据,从⽽实现⾼效查找数据。

在数据库系统中建立索引主要有以下作用:

(1)快速取数据;

(2)保证数据记录的唯一性;

(3)实现表与表之间的参照完整性;

(4)在使用ORDER by、group by子句进行数据检索时,利用索引可以减少排序和分组的时间

优点

1.大大加快数据的检索速度;

2.创建唯一性索引,保证数据库表中每一行数据的唯一性;

3.加速表和表之间的连接;

4.在使用分组和排序子句进行数据检索时,

2.当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。

1.1索引的类型

从应用层次划分:普通索引、唯一索引、主键索引、复合索引
1.1.1普通索引

这是最基本的索引类型,基于普通字段建立的索引,没有任何限制。
创建普通索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名);
CREATE TABLE tablename ( […], INDEX [索引的名字] (字段名) );

1.1.2唯一索引

与”普通索引”类似,不同的就是:索引字段的值必须唯一,但允许有空值 。在创建或修改表时追加唯一约束,就会自动创建对应的唯一索引。
创建唯一索引的方法如下:
CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD UNIQUE INDEX [索引的名字] (字段名);
CREATE TABLE tablename ( […], UNIQUE [索引的名字] (字段名) ;

1.1.3主键索引

它是一种特殊的唯一索引,不允许有空值。在创建或修改表时追加主键约束即可,每个表只能有一个主
键。
创建主键索引的方法如下:
CREATE TABLE tablename ( […], PRIMARY KEY (字段名) );
ALTER TABLE tablename ADD PRIMARY KEY (字段名);

1.1.4复合索引

单一索引是指索引列为一列的情况下,即新建索引的语句只实施在一列上;用户可以在多个列上建立索引,这种索引叫做复合索引(组合索引)。复合索引可以代替多个单一索引,相比多个单一索引复合索引所需的开销更小

做窄索引和宽索引,窄索引是指索引列为1-2列的索引,宽索引也就是索引列超
过2列的索引,设计索引的一个重要原则就是能用窄索引不用宽索引,因为窄索引往往比组合索引更有效。

创建组合索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2…);
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名1,字段名2…);CREATE TABLE tablename ( […], INDEX [索引的名字] (字段名1,字段名2…) );

复合索引使用注意事项:
何时使用复合索引,要根据where条件建索引,注意不要过多使用索引,过多使用会对更新操作效
率有很大影响。
如果表已经建立了(col1,col2),就没有必要再单独建立(col1);如果现在有(col1)索引,如果查
询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。

1.1.5全文索引

查询操作在数据量比较少时,可以使用like模糊查询,但是对于大量的文本数据检索,效率很低。如果使用全文索引,查询速度会比like快很多倍。在MySQL 5.6 以前的版本,只有MyISAM存储引擎支持全文索引,从MySQL 5.6开始MyISAM和InnoDB存储引擎均支持。

创建全文索引的方法如下:
CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD FULLTEXT [索引的名字] (字段名);
CREATE TABLE tablename ( […], FULLTEXT KEY [索引的名字] (字段名) ;
和常用的like模糊查询不同,全文索引有自己的语法格式,使用 match 和 against 关键字,比如
全文索引使用注意事项:
全文索引必须在字符串、文本字段上建立。
全文索引字段值必须在最小字符和最大字符之间的才会有效。(innodb:3-84;myisam:4-
84)
全文索引字段值要进行切词处理,按syntax字符进行切割,例如b+aaa,切分成b和aaa
全文索引匹配查询,默认使用的是等值匹配,例如a匹配a,不会匹配ab,ac。如果想匹配可以在布
尔模式下搜索a*

select from user
where match(name) against(‘a
‘ in boolean mode);

1.2索引原理

MySQL官方对索引的定义:是存储引擎用于快速查找记录的一种数据结构,需要额外开辟空间和数据维护工作

  • 索引是物理数据页存储,在数据文件中(InnoDB,ibd文件),利用数据页(page)存储
  • 索引可以加快检索速度,但是同时会降低增删改的操作速度,索引维护需要代价

索引设计的理论知识:二分查找法、Hash和B+Tree

二分查找法

二分查找法也叫做折中查找法,它是在有序数组中查找指定数据的搜索算法,它的优点是等值查询、范围查询性能优秀,缺点是更新数据、新增数据、删除数据维护成本高。

Hash

Hash底层是由Hash表实现的,是根据键值 存储数据的结构。非常适合根据key查找value值,也就是单个key查询,或者说等值查询。其结构如下所示:

从上面的结构可以看出,Hash索引可以方便等值查询,但与对于范围查询就需要全表扫描了。

Hash索引在MySQL中Hash结构主要应用在Memory原生的Hash索引、InnoDB自适应哈希索引。

InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引(执行效率取决于树的高度,一般高度是3-4层,与数据量的多少有关系)再创建一个哈希索引,使得内存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。

InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据,等值查询效率要优于B+Tree。自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。

  1. --查看自适应哈希索引状态
  2. show engine innodb status \G;
  3. --如果使用了,比没使用效率要低,可以进行手动关闭
  4. show variables like '%innodb_adaptive%';
  5. +----------------------------------+--------+
  6. | Variable_name | Value |
  7. +----------------------------------+--------+
  8. | innodb_adaptive_flushing | ON |
  9. | innodb_adaptive_flushing_lwm | 10 |
  10. | innodb_adaptive_hash_index | ON |
  11. | innodb_adaptive_hash_index_parts | 8 |
  12. | innodb_adaptive_max_sleep_delay | 150000 |
  13. +----------------------------------+--------+
  14. set global innodb_adaptive_hash_index=0;
  15. show variables like '%innodb_adaptive%';
  16. +----------------------------------+--------+
  17. | Variable_name | Value |
  18. +----------------------------------+--------+
  19. | innodb_adaptive_flushing | ON |
  20. | innodb_adaptive_flushing_lwm | 10 |
  21. | innodb_adaptive_hash_index | OFF |
  22. | innodb_adaptive_hash_index_parts | 8 |
  23. | innodb_adaptive_max_sleep_delay | 150000 |
  24. +----------------------------------+--------+

B+Tree

MySQL数据库索引采用的是B+Tree结构,在B-Tree结构上做了优化改造。

  • B-Tree结构
    • 索引值和data数据分布在整棵树结构中
    • 每个节点可以存放多个索引值及对应的data数据
    • 数据树节点中的多个索引值从左到右升序排列

B树的搜索:从根节点开始,对节点内的索引值序列采用二分法查找,如果命中就结束查找。没有命中会进入子节点重复查找过程,直到所对应的的节点指针为空,或已经是叶子节点了才结束。

  • B+Tree结构
    • 非叶子节点不存储data数据,只存储索引值,这样便于存储更多的索引值
    • 叶子节点包含了所有的索引值和data数据
    • 叶子节点用指针连接,提高区间的访问性能

相比B树,B+树进行范围查找时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进行遍历即可。而B树需要遍历范围内所有的节点和数据,显然B+Tree效率高。

聚簇索引和辅助索引

聚簇索引和非聚簇索引:B+Tree的叶子节点存放主键索引值和行记录就属于聚簇索引;如果索引值和行记录分开存放就属于非聚簇索引。

主键索引和辅助索引:B+Tree的叶子节点存放的是主键字段值就属于主键索引;如果存放的是非主键值就属于辅助索引(二级索引)。

在InnoDB引擎中,主键索引采用的就是聚簇索引结构存储。

  • 聚簇索引(聚集索引)
    聚簇索引是一种数据存储方式,InnoDB的聚簇索引就是按照主键顺序构建 B+Tree结构。B+Tree的叶子节点就是行记录,行记录和主键值紧凑地存储在一起。 这也意味着 InnoDB 的主键索引就是数据表本身,它按主键顺序存放了整张表的数据,占用的空间就是整个表数据量的大小。通常说的主键索引就是聚集索引。
    InnoDB的表要求必须要有聚簇索引:
    • 如果表定义了主键,则主键索引就是聚簇索引
    • 如果表没有定义主键,则第一个非空unique列作为聚簇索引
    • 否则InnoDB会从建一个隐藏的row-id作为聚簇索引
  • 辅助索引
    InnoDB辅助索引,也叫作二级索引,是根据索引列构建 B+Tree结构。但在 B+Tree 的叶子节点中只存了索引列和主键的信息。二级索引占用的空间会比聚簇索引小很多, 通常创建辅助索引就是为了提升查询效率。一个表InnoDB只能创建一个聚簇索引,但可以创建多个辅助索引。
  • 非聚簇索引
    与InnoDB表存储不同,MyISAM数据表的索引文件和数据文件是分开的,被称为非聚簇索引结构。

1.3索引分析和优化

explain

MySQL 提供了一个 EXPLAIN 命令,它可以对 SELECT 语句进行分析,并输出 SELECT 执行的详细信

息,供开发人员有针对性的优化。例如:

  1. --\G 结果以纵向结构展示
  2. explain select * from sys_user where username='admin' \G;
  3. *************************** 1. row ***************************
  4. id: 1
  5. select_type: SIMPLE
  6. table: sys_user
  7. partitions: NULL
  8. type: ALL
  9. possible_keys: idx_username
  10. key: NULL
  11. key_len: NULL
  12. ref: NULL
  13. rows: 2
  14. filtered: 50.00
  15. Extra: Using where
  16. 1 row in set, 1 warning (0.00 sec)
  • select_type
    表示查询的类型,常见值:
    • SIMPLE : 表示查询语句不包含子查询或union
    • PRIMARY:表示此查询是最外层的查询
    • UNION:表示此查询是UNION的第二个或后续的查询
    • DEPENDENT UNION:UNION中的第二个或后续的查询语句,使用了外面查询结果
    • UNION RESULT:UNION的结果
    • SUBQUERY:SELECT子查询语句
    • DEPENDENT SUBQUERY:SELECT子查询语句依赖外层查询的结果。

最常见的查询类型是SIMPLE,表示我们的查询没有子查询也没用到UNION查询。

  • type
    表示存储引擎查询数据时采用的方式。比较重要的一个属性,通过它可以判断出查询是全表扫描还是基于索引的部分扫描。常用属性值如下,从上至下效率依次增强。
    • ALL:表示全表扫描,性能最差。
    • index:表示基于索引的全表扫描,先扫描索引再扫描全表数据。
    • range:表示使用索引范围查询。使用>、>=、<、<=、in等等。
    • ref:表示使用非唯一索引进行单值查询。
    • eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果。
    • const:表示使用主键或唯一索引做等值查询,常量查询。
    • NULL:表示不用访问表,速度最快。
  • possible_keys
    表示查询时能够使用到的索引。注意并不一定会真正使用,显示的是索引名称。
  • key
    表示查询时真正使用到的索引,显示的是索引名称。
  • rows
    MySQL查询优化器会根据统计信息,估算SQL要查询到结果需要扫描多少行记录。原则上rows是越少效率越高,可以直观的了解到SQL效率高低。
  • key_len
    表示查询使用了索引的字节数量。可以判断是否全部使用了组合索引。
    key_len的计算规则如下:
    • 字符串类型
      字符串长度跟字符集有关:latin1=1、gbk=2、utf8=3、utf8mb4=4
      char(n):n字符集长度
      varchar(n):n
      字符集长度 + 2字节
    • 数值类型
      TINYINT:1个字节
      SMALLINT:2个字节
      MEDIUMINT:3个字节
      INT、FLOAT:4个字节
      BIGINT、DOUBLE:8个字节
    • 时间类型
      DATE:3个字节
      TIMESTAMP:4个字节
      DATETIME:8个字节
    • 字段属性
      NULL属性占用1个字节,如果一个字段设置了NOT NULL,则没有此项。
  • Extra
    Extra表示很多额外的信息,各种操作会在Extra提示相关信息,常见几种如下:
    • Using where
      表示查询需要通过索引回表查询数据。
    • Using index
      表示查询需要通过索引,索引就可以满足所需数据。
    • Using fifilesort
      表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,因此有Using fifilesort建议优化。
    • Using temprorary
      查询使用到了临时表,一般出现于去重、分组等操作。

回表查询

InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点存储行记录,InnoDB必须要有,且只有一个。辅助索引的叶子节点存储的是主键值和索引字段值,通过辅助索引无法直接定位行记录,通常情况下,需要扫码两遍索引树。先通过辅助索引定位主键值,然后再通过聚簇索引定位行记录,这就叫做回表查询,它的性能比扫一遍索引树低。

总结:通过索引查询主键值,然后再去聚簇索引查询记录信息。

覆盖索引

在MySQL官网,类似的说法出现在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖。

不管是SQL-Server官网,还是MySQL官网,都表达了:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快,这就叫做索引覆盖

实现索引覆盖最常见的方法就是:将被查询的字段,建立到组合索引。

最左前缀索引

复合索引使用时遵循最左前缀原则,最左前缀顾名思义,就是最左优先,即查询中使用到最左边的列,那么查询就会使用到索引,如果从索引的第二列开始查找,索引将失效。

like查询

面试题:MySQL在使用like模糊查询时,索引能不能起作用?

MySQL在使用Like模糊查询时,索引是可以被使用的,只有把%字符写在后面才会使用到索引。

  1. select * from user where name like '%o%'; //不起作用
  2. select * from user where name like 'o%'; //起作用
  3. select * from user where name like '%o'; //不起作用

null查询

面试题:如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?

对MySQL来说,NULL是一个特殊的值,从概念上讲,NULL意味着“一个未知值”,它的处理方式与其他值有些不同。比如:不能使用=,<,>这样的运算符,对NULL做算术运算的结果都是NULL,count时不会包括NULL行等,NULL比空字符串需要更多的存储空间等。

  1. NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.”

NULL列需要增加额外空间来记录其值是否为NULL。对于MyISAM表,每一个空列额外占用一位,四舍五入到最接近的字节。

虽然MySQL可以在含有NULL的列上使用索引,但NULL和其他数据还是有区别的,不建议列上允许为NULL。最好设置NOT NULL,并给一个默认值,比如0和 ‘’ 空字符串等,如果是datetime类型,也可以设置系统当前时间或某个固定的特殊值,例如’1970-01-01 00:00:00’。

排序与索引

MySQL查询支持fifilesort和index两种方式的排序,fifilesort是先把结果查出,然后在缓存或磁盘进行排序操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。

fifilesort有两种排序算法:双路排序和单路排序。

双路排序:需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二次去读取其他字段数据。

单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存sort_buffffer,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO,反而会增加负担。解决方案:少使用select *;增加sort_buffffer_size容量和max_length_for_sort_data容量。

如果Explain分析SQL,结果中Extra属性显示Using fifilesort,表示使用了fifilesort排序方式,需要优化。如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用index排序方式,建议尽可能采用覆盖索引。

  • 以下几种情况,会使用index方式的排序。

    • ORDER BY 子句索引列组合满足索引最左前列

      1. explain select id from user order by id; //对应(id)、(id,name)索引有效
    • WHERE子句+ORDER BY子句索引列组合满足索引最左前列

      1. explain select id from user where age=18 order by name; //对应 (age,name)索引
  • 以下几种情况,会使用fifilesort方式的排序。

    • 对索引列同时使用了ASC和DESC

      1. explain select id from user order by age asc,name desc; //对应 (age,name)索引
    • WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询(例如>、<、in等)

      1. explain select id from user where age>10 order by name; //对应 (age,name)索引
    • ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列

      1. explain select id from user order by name; //对应(age,name)索引
    • 使用了不同的索引,MySQL每次只采用一个索引,ORDER BY涉及了两个索引

      1. explain select id from user order by name,age; //对应(name)、(age)两个索引
    • WHERE子句与ORDER BY子句,使用了不同的索引

      1. explain select id from user where name='tom' order by age; //对应 (name)、(age)索引
    • WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式

      1. explain select id from user order by abs(age); //对应(age)索引

小结:

问题场景:索引失效场景有哪些,如何避免?

  1. 索引列不独立——>可以提前计算好结果
    ```mysql —索引列参与表达式计算 select id,name,age,salary from table_name where salary + 1000 = 6000; —修改为: select id,name,age,salary from table_name where salary = 5000;

—索引字段作为函数的参数 select id,name,age from table_name where substring(name,1,3)= ‘luc’; —修改为: select id,name,age from table_name where name like ‘luc%’;

  1. 2. 使用了左模糊---->使用搜索引擎es
  2. ```mysql
  3. select id,name,age from table_name where name like '%luc%';
  1. 使用 or 查询部分字段没有使用索引——>or两边字段都建索引

    1. select id,name,age from table_name where name ='lucy' and age >25;
  2. 字符串条件没有使用 ‘ ‘——>检查SQL,修改SQL

    1. select id,name,age,salary from table_name where phone=13088772233;
    2. --修改为
    3. select id,name,age,salary from table_name where phone='13088772233 ';
  3. 不符合最左前缀原则的查询——>组合索引 index(a,b,c),按索引顺序查询,

    1. select * from table_name where a = 'asaa' and b='1'and c='2';--可以走索引
    2. select * from table_name where a = 'asda' and b='1231'; --可以走索引
    3. select * from table_name where b='1'and c='2';--无法走索引
    4. select * from table_name where c='2';--无法走索引
    5. select * from table_name where a = 'asda' and c='dsfsdafsfsd';--部分走索引
  4. 索引字段没有添加 not null 约束——>可以将需要索引的字符串设置为 not null default ‘ ‘ 默认空字符串即可

    1. select * from table_name where a is null;--这条sql就无法走索引执行了,is null 条件不能使用索引,只能全表扫描了,mysql 官方建议是把字段设置为 not null
  5. 隐式转换导致索引失效

    1. select * from table_name t1 left join table_name2 t2 on t1.id=t2.tid;--如果t1表的idt2表的tid字段类型不一致,无法走索引,解决方法是统一设置字段类型

1.4查询优化

慢查询定位
  • 开启慢查询日志
    查看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:
    通过如下命令开启慢查询日志:
    ```mysql SHOW VARIABLES LIKE ‘slow_query_log%’;

+——————————-+—————————————+ | Variable_name | Value | +——————————-+—————————————+ | slow_query_log | ON | | slow_query_log_file | LAPTOP-CAG7OTGF-slow.log | +——————————-+—————————————+ 2 rows in set, 1 warning (0.00 sec)

  1. ```mysql
  2. SET global slow_query_log = ON;
  3. SET global slow_query_log_file = 'OAK-slow.log';
  4. SET global log_queries_not_using_indexes = ON;
  5. SET long_query_time = 10;
  • long_query_time:指定慢查询的阈值,单位秒。如果SQL执行时间超过阈值,就属于慢查询记录到日志文件中。
    • log_queries_not_using_indexes:表示会记录没有使用索引的查询SQL。前提是slow_query_log的值为ON,否则不会奏效。
    • 查看慢查询日志
  • 文本方式查看
    直接使用文本编辑器打开slow.log日志即可。
    image-20211113214636258
    • time:日志记录的时间
    • User@Host:执行的用户及主机
    • Query_time:执行的时间
    • Lock_time:锁表时间
    • Rows_sent:发送给请求方的记录数,结果数量
    • Rows_examined:语句扫描的记录条数
    • SET timestamp:语句执行的时间点
    • select….:执行的具体的SQL语句
  • 使用mysqldumpslow查看
    MySQL 提供了一个慢查询日志分析工具mysqldumpslow,可以通过该工具分析慢查询日志内容。
    在 MySQL bin目录下执行下面命令可以查看该使用格式。
    运行如下命令查看慢查询日志信息:
    除了使用mysqldumpslow工具,也可以使用第三方分析工具,比如pt-query-digest、mysqlsla等。
    1. perl mysqldumpslow.pl --help
    1. perl mysqldumpslow.pl -t 5 -s at C:\ProgramData\MySQL\Data\OAK-slow.log

慢查询优化
  • 索引和慢查询

    • 如何判断是否为慢查询?
      MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执行时间跟 long_query_time 参数做比较,如果语句的执行时间 > long_query_time,就会把这条执行语句记录到慢查询日志里面。long_query_time 参数的默认值是 10s,该参数值可以根据自己的业务需要进行调整。
    • 如何判断是否应用了索引?
      SQL语句是否使用了索引,可根据SQL语句执行过程中有没有用到表的索引,可通过 explain命令分析查看,检查结果中的 key 值,是否为NULL。
    • 应用了索引是否一定快?
      下面我们来看看下面语句的 explain 的结果,你觉得这条语句有用上索引吗?比如
      虽然使用了索引,但是还是从主键索引的最左边的叶节点开始向右扫描整个索引树,进行了全表扫描,此时索引就失去了意义。
      而像 select * from user where id = 2; 这样的语句,才是我们平时说的使用了索引。它表示的意思是,我们使用了索引的快速搜索功能,并且有效地减少了扫描行数。
      1. select * from user where id>0;
      查询是否使用索引,只是表示一个SQL语句的执行过程;而是否为慢查询,是由它执行的时间决定的,也就是说是否使用了索引和是否是慢查询两者之间没有必然的联系。
      我们在使用索引时,不要只关注是否起作用,应该关心索引是否减少了查询扫描的数据行数,如果扫描行数减少了,效率才会得到提升。对于一个大表,不止要创建索引,还要考虑索引过滤性,过滤性好,执行速度才会快。
  • 提高索引过滤性
    假如有一个5000万记录的用户表,通过sex=’男’索引过滤后,还需要定位3000万,SQL执行速度也不会很快。其实这个问题涉及到索引的过滤性,比如1万条记录利用索引过滤后定位10条、100条、1000条,那他们过滤性是不同的。索引过滤性与索引字段、表的数据量、表设计结构都有关系

    • 下面我们看一个案例:

      1. 表:student
      2. 字段:id,name,sex,age
      3. 造数据:insert into student (name,sex,age) select name,sex,age from student;
      4. SQL案例:select * from student where age=18 and name like '张%';(全表扫 描)
    • 优化1

      1. alter table student add index(name); //追加name索引
    • 优化2

      1. alter table student add index(age,name); //追加age,name索引
    • 优化3

      1. //为user表添加first_name虚拟列,以及联合索引(first_name,age)
      2. alter table student add first_name varchar(2) generated always as (left(name, 1)), add index(first_name, age);
      3. explain select * from student where first_name='张' and age=18;
  • 慢查询原因总结

    • 全表扫描:explain分析type属性all
    • 全索引扫描:explain分析type属性index
    • 索引过滤性不好:靠索引字段选型、数据量和状态、表设计
    • 频繁的回表查询开销:尽量少用select *,使用覆盖索引

分页查询优化
  • 一般性分页
    一般的分页查询使用简单的 limit 子句就可以实现。limit格式如下:

    1. SELECT * FROM 表名 LIMIT [offset,] rows
    • 第一个参数指定第一个返回记录行的偏移量,注意从0开始;
    • 第二个参数指定返回记录行的最大数目;
    • 如果只给定一个参数,它表示返回最大的记录行数目

思考1:如果偏移量固定,返回记录量对执行时间有什么影响?

  1. select * from user limit 10000,1;
  2. select * from user limit 10000,10;
  3. select * from user limit 10000,100;
  4. select * from user limit 10000,1000;
  5. select * from user limit 10000,10000;

结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录量越大,所花费的时间也会越来越多。
思考2:如果查询偏移量变化,返回记录数固定对执行时间有什么影响?

  1. select * from user limit 1,100;
  2. select * from user limit 10,100;
  3. select * from user limit 100,100;
  4. select * from user limit 1000,100;
  5. select * from user limit 10000,100;

结果:在查询记录时,如果查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间急剧的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而且查询的数据越多,也会拖慢总查询速度。)

  • 分页优化方案
    第一步:利用覆盖索引优化
    第二步:利用子查询优化
    原因:使用了id做主键比较(id>=),并且子查询使用了覆盖索引进行优化。
    1. select * from user limit 10000,100;
    2. select id from user limit 10000,100;
    1. select * from user limit 10000,100;
    2. select * from user where id>= (select id from user limit 10000,1) limit 100;

事务和锁

2.1事务特性

事务有4大特性:ACID,即原子性,一致性,隔离性和持久性。

原子性

概念

事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

如何保证原子性?

事务提交的过程

数据修改—>Buffer Pool修改—>刷盘

可以分如下两种常见进行讨论:

1、数据修改并提交了事务,但是此时Buffer Pool的脏页没有刷盘,如何保证修改的数据生效?

2、数据修改但事务未提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销?

针对1,可以利用Redo日志来保证修改的数据生效;

针对2,可以利用Undo日志来保证不该存在的数据撤销。

之所以可以利用Redo和Undo日志,是利用了Write-Ahead Logging(WAL)机制,简单说就是先写日志,再写磁盘。保证了未写入磁盘的数据,可以从Redo或Undo中找回,并根据事务是否提交成功来决定是生效还是撤销。

疑问1:写日志是同步写还是异步写?

是异步写的,Redo日志有对应刷新redo日志的线程,而undo日志也有对应回收undo页的线程。按照一定的频率写入磁盘。

疑问2:如果修改的数据,还未写日志,mysql就挂了呢?是不是数据就丢失了?

如果在刷新Redo日志的线程刷新前,mysql挂了的话,数据是会丢失的;

同样的,如果在回收undo页的线程回收后,mysql挂了的话,数据是不是也无法撤销?实践上,由于undo页只在事务提交之后才进行回收,而此时的数据是应该被保存的,而无需撤销。所以理论上不会出现数据无法撤销的情形。

疑问3:使用了WAL技术,为何比直接更新磁盘快?

直接更新磁盘,需要找到对应的那条数据再更新,是随机IO。

而使用了WAL技术,是可以直接追加写日志的,不需要考虑数据位置,是顺序IO,并且可以批量提交。

顺序IO的性能比随机IO好很多。

一致性

概念

指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内 容,分别是约束一致性和数据一致性。

  • 约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束,可惜在 MySQL 中不支持 Check 。
  • 数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是 单单依赖于某一种技术。

如何保证一致性?

一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个 特性又是通过 Redo/Undo 来保证的。逻辑上的一致性,包括唯一索引、外键约束、check 约束,这属 于业务逻辑范畴。

隔离性

概念

指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并 发事务是隔离的。

如何保证隔离性?

InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。锁 和多版本控制(MVCC)技术就是用于保障隔离性的。

行锁的原理:对索引数据页上的记录加锁实现的。

MVCC的原理:

  • 数据行里的DB_ROW_ID(隐含ID)、DB_TRX_ID(事务ID)和DB_ROLL_PTR(回滚指针)
  • undo日志
  • read view

持久性

概念

指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不 应该对其有任何影响,不会丢失。

如何保证持久性?

MySQL的持久性也与WAL技术相关,通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持久性。

2.2事务控制的演进

并发事务

事务并发处理可能会带来一些问题,比如:更新丢失、脏读、不可重复读、幻读等。

  • 更新丢失 当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。
    • 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。
    • 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。
  • 脏读 一个事务读取到了另一个事务修改但未提交的数据。
  • 不可重复读 一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。
  • 幻读 一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了 几行记录。

排队

最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列 化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。

排他锁

引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥 锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。

注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。

读写锁

读和写操作:读读、写写、读写、写读。 读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁。

读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。

MVCC

多版本控制MVCC,也就是Copy on Write的思想。MVCC除了支持读和读并行,还支持读和写、写和读 的并行,但为了保证一致性,写和写是无法并行的。

在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影 响其他事务对此记录的读取,实现写和读并行。

概念

MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的 数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。

优缺点

MVCC最大的好处是读不加锁,读写不冲突,大大提高了数据库的吞吐量及读写性能。不过目前MVCC只在 Read Commited 和 Repeatable Read 两种隔离级别下工作。

延伸:为何只在 Read Commited和Repeatable Read 这两种隔离级别下工作?

我们知道隔离级别有4种,除了Read Commited和Repeatable Read,还有Read Uncommitted和Serializable。先说Read Uncommitted,由于MVCC是通过记录事务提交前后的历史版本来实现的,如果用Read Uncommitted,事务提交前和事务提交后,读到的内容是一样的,也就无所谓版本了。

再说Serializable,由于是串行化,每个事务都是按顺序进行的,不存在并发事务,也就无法利用MVCC的特性。

特性

在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。

  • 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。(select)
  • 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发 修改这条记录。(select… for update 或lock in share mode,insert/delete/update)

如何生成的多版本?

每次事务修改操作之前,都会在Undo日志中记录修改之前的数据状态和事务号,该备份记录可以用于其他事务的读取,也可以进行必要时的数据回滚。

3.1. 锁分类

3.1.1. 操作的粒度可分为表级锁、行级锁和页级锁

  • 表级锁:
    每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB 等存储引擎中
  • 行级锁:
    每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。
  • 页级锁:
    每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。应用在BDB 存储引擎中。 | | 行锁 | 表锁 | 页锁 | | —- | —- | —- | —- | | MyISAM | | 存在 | | | BDB | | 存在 | 存在 | | InnoDB | 存在 | 存在 | |

3.1.2. 从操作的类型可分为读锁和写锁

  • 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。
  • 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。

3.1.3. 从操作的性能可分为乐观锁和悲观锁

  • 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
  • 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。

3.2. 行锁原理

InnoDB行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。

  • RecordLock锁:锁定单个行记录的锁。(记录锁,RC、RR隔离级别都支持)
  • GapLock锁:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。(范围锁,RR隔离级别支持)
  • Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持)

在RR隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引 时,Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非范围。 1)select … from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句,InnoDB不加锁

2)select … from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

3)select … from for update语句:追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

4)update … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

5)delete … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

6)insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock

以“update t1 set name=‘XX’ where id=10”操作为例

3.2.1. 主键加锁

100%

取消上传

仅在id=10的主键索引记录上加X锁

3.2.2. 唯一键加锁

100%

取消上传

在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁

3.2.3. 非唯一键加锁

100%

取消上传

对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)- (11,f)范围分别加Gap Lock

3.2.4. 无索引加锁

100%

取消上传

表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎锁机制是基于索引实现的记录锁定)

3.3. 悲观锁

是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁范畴。

3.3.1. 表级锁

表级锁每次操作都锁住整张表,并发度最低

手动增加表锁:

  1. lock table 表名称 read|write,表名称2 read|write;

查看表上加过的锁:

  1. show open tables;

删除表锁:

  1. unlock tables;

表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作会报错,其他连接增删改会被阻塞。

表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都被阻塞(包括查询)

总结:表级读锁会阻塞写操作,但是不会阻塞读操作;而写锁则会把读和写操作都阻塞

3.3.2. 共享锁(行级锁-读锁)

共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

使用共享锁的方法:

  1. select ... lock in share mode,只适用查询语句。

总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。

3.3.3. 排他锁(行级锁-写锁)

排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排 他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。

使用排他锁 :

  1. SQL末尾加上for updateinnodb引擎默认会在updatedelete语句加上for update

行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记录

总结:事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录锁(**select... for update**)。如果查询没有使用到索引,将会锁住整个表记录

3.4. 乐观锁

它不是数据库提供的功能,需要开发者自己去实现

在数据库操作时, 想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁, 而是在进行事务提交时再去判断是否有冲突

对并发率要求高的选择乐观锁;对于并发率要求低的可以选择悲观锁

  • 乐观锁实现原理
    • 使用版本字段(version)
      先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version 是用来查看被读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改
      100%
    • 使用时间戳(Timestamp)
      与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp 时间戳。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则提交更新,否则就是版本冲突,取消操作

案例:

查询商品信息 :

  1. select (quantity,version) from products where id=1;

根据商品信息生成订单

  1. insert into orders ...
  2. insert into items ...

修改商品库存

  1. update products set quantity=quantity-1,version=version+1 where id=1 and version=#{version};

除了自己手动实现乐观锁之外,许多数据库访问框架也封装了乐观锁的实现,比如 hibernate框架。MyBatis框架可以使用OptimisticLocker插件来扩展

3.5. 死锁与解决方案

3.5.1. 表锁死锁

用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了

用户A—> A表(表锁)—> B表(表锁)

用户B—> B表(表锁)—> A表(表锁)

解决方案:

对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个 资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源

3.5.2. 行级锁死锁

  • 原因1

在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等 价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁

  • 解决方案:

SQL语句中不要使用太复杂的关联多表的查询使用explain“执行计划”对SQL语句进行分析,对于有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化

  • 原因2

两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁

100%

取消上传

  • 解决方案:
    • 在同一个事务中,尽可能做到一次锁定所需要的所有资源
    • 按照id对资源排序,然后按顺序进行处理

3.5.3. 共享锁转换为排他锁

  • 产生原因:

事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时, 此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经有一个排他锁请求,并且正在等待事务A 释放其共享锁

  1. 事务A:
  2. select * from dept where deptno=1 lock in share mode; //共享锁,1
  3. update dept set dname='java' where deptno=1;//排他锁,3
  4. 事务B:
  5. update dept set dname='Java' where deptno=1;//由于1有共享锁,没法获取排他锁,需等待,2
  • 解决方案
    • 对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操 作;
    • 使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用 户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;

3.5.4. 死锁排查

  • 查看死锁日志

通过show engine innodb status \G命令查看近期死锁日志信息。

使用方法:1、查看近期死锁日志信息;2、使用explain查看下SQL执行计划

  • 查看锁状态变量
    通过show status like'innodb_row_lock%‘命令检查状态变量,分析系统中的行锁的争夺情况
    • Innodb_row_lock_current_waits:当前正在等待锁的数量
    • Innodb_row_lock_time:从系统启动到现在锁定总时间长度
    • Innodb_row_lock_time_avg: 每次等待锁的平均时间
    • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
    • Innodb_row_lock_waits:系统启动后到现在总共等待的次数

如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着手定制优化

分库分表

使用分库分表时,主要有垂直拆分和水平拆分两种拆分模式,都属于物理空间的拆分。
分库分表方案:只分库、只分表、分库又分表。 垂直拆分:由于表数量多导致的单个库大。将表拆分到多个库中。
水平拆分:由于表记录多导致的单个库大。将表记录拆分到多个表中。

4.1拆分方式

  • 垂直拆分垂直拆分又称为纵向拆分,垂直拆分是将表按库进行分离,或者修改表结构按照访问的差异将某些 列拆分出去。应用时有垂直分库和垂直分表两种方式,一般谈到的垂直拆分主要指的是垂直分库垂直分表就是将一张表中不常用的字段拆分到另一张表中,从而保证第一张表中的字段较少,避免 出现数据库跨页存储的问题,从而提升查询效率。 解决:一个表中字段过多,还有有些字段经常使用,有些字段不经常使用,或者还有text等字段信 息。可以考虑使用垂直分表方案。按列进行垂直拆分,即把一条记录分开多个地方保存,每个子表的行数相同。把主键和一些列放到 一个表,然后把主键和另外的列放到另一个表中。
    • 垂直拆分优点:
      • 拆分后业务清晰,拆分规则明确; 易于数据的维护和扩展;
      • 可以使得行数据变小,一个数据块 (Block) 就能存放更多的数据,在查询时就会减少 I/O 次 数;
      • 可以达到最大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将 经常改变的放一起;
      • 便于实现冷热分离的数据表设计模式。
    • 垂直拆分缺点:
      • 主键出现冗余,需要管理冗余列;
      • 会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据库压力,提高了系 统的复杂度;
      • 依然存在单表数据量过大的问题; 事务处理复杂。
  • 水平拆分水平拆分又称为横向拆分。 相对于垂直拆分,它不再将数据根据业务逻辑分类,而是通过某个字 段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个表仅包含数据的一部分,水平分表是将一张含有很多记录数的表水平切分,不同的记录可以分开保存,拆分成几张结构相同 的表。如果一张表中的记录数过多,那么会对数据库的读写性能产生较大的影响,虽然此时仍然能 够正确地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个 问题。 水平拆分:解决表中记录过多问题。 垂直拆分:解决表过多或者是表字段过多问题。水平拆分重点考虑拆分规则:例如范围、时间或Hash算法等。

    • 水平拆分优点:
      • 拆分规则设计好,join 操作基本可以数据库做; 不存在单库大数据,高并发的性能瓶颈;
      • 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可; 提高了系统的稳定性和负载能力。
    • 水平拆分缺点:
      • 拆分规则难以抽象;
      • 跨库Join性能较差;
      • 分片事务的一致性难以解决; 数据扩容的难度和维护量极大。

        4.2主键策略

  • UUID

  • SNOWFLAKE
  • 数据库ID表
  • Redis生成ID

    4.3分片策略

  • 分片概念分片(Sharding)就是用来确定数据在多台存储设备上分布的技术。Shard这个词的意思是“碎片”,如 果将一个数据库当作一块大玻璃,将这块玻璃打碎,那么每一小块都称为数据库的碎片(Database Sharding)。将一个数据库打碎成多个的过程就叫做分片,分片是属于横向扩展方案。 分片:表示分配过程,是一个逻辑上概念,表示如何实现 分库分表:表示分配结果,是一个物理上概念,表示最终实现的结果 数据库扩展方案: 横向扩展:一个库变多个库,加机器数量 纵向扩展:一个库还是一个库,优化机器性能,加高配CPU或内存 在分布式存储系统中,数据需要分散存储在多台设备上,分片就是把数据库横向扩展到多个数据库服务 器上的一种有效的方式,其主要目的就是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展 性问题。

  • 分片策略数据分片是根据指定的分片键和分片策略将数据水平拆分,拆分成多个数据片后分散到多个数据存储节 点中。分片键是用于划分和定位表的字段,一般使用ID或者时间字段。而分片策略是指分片的规则,常 用规则有以下几种

    • 基于范围分片根据特定字段的范围进行拆分,比如用户ID、订单时间、产品价格等。例如: {[1 - 100] => Cluster A, [101 - 199] => Cluster B} 优点:新的数据可以落在新的存储节点上,如果集群扩容,数据无需迁移。 缺点:数据热点分布不均,数据冷热不均匀,导致节点负荷不均。
    • 哈希取模分片整型的Key可直接对设备数量取模,其他类型的字段可以先计算Key的哈希值,然后再对设备数量 取模。假设有n台设备,编号为0 ~ n-1,通过Hash(Key) % n就可以确定数据所在的设备编号。该 模式也称为离散分片。优点:实现简单,数据分配比较均匀,不容易出现冷热不均,负荷不均的情况。 缺点:扩容时会产生大量的数据迁移,比如从n台设备扩容到n+1,绝大部分数据需要重新分配和 迁移。
    • 一致性哈希分片采用Hash取模的方式进行拆分,后期集群扩容需要迁移旧的数据。使用一致性Hash算法能够很大 程度的避免这个问题,所以很多中间件的集群分片都会采用一致性Hash算法。 一致性Hash是将数据按照特征值映射到一个首尾相接的Hash环上,同时也将节点(按照IP地址或 者机器名Hash)映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节 点即为数据的存储节点。一致性Hash在增加或者删除节点的时候,受到影响的数据是比较有限的,只会影响到Hash环相邻的节 点,不会发生大规模的数据迁移。

      4.4扩容方案

      当系统用户进入了高速增长期时,即便是对数据进行分库分表,但数据库的容量,还有表的数据量也总 会达到天花板。当现有数据库达到承受极限时,就需要增加新服务器节点数量进行横向扩容。 首先来思考一下,横向扩展会有什么技术难度?
      数据迁移问题
      分片规则改变 数据同步、时间点、数据一致性
  • 停机扩容这是一种很多人初期都会使用的方案,尤其是初期只有几台数据库的时候。停机扩容的具体步骤如下:优点:简单 缺点: 停止服务,缺乏高可用 程序员压力山大,需要在指定时间完成 如果有问题没有及时测试出来启动了服务,运行后发现问题,数据会丢失一部分,难以回滚。 适用场景: 小型网站 大部分游戏 对高可用要求不高的服务

    • 站点发布一个公告,例如:“为了为广大用户提供更好的服务,本站点将在今晚00:00-2:00之间升 级,给您带来不便抱歉”;
    • 时间到了,停止所有对外服务;
    • 新增n个数据库,然后写一个数据迁移程序,将原有x个库的数据导入到最新的y个库中。比如分片 规则由%x变为%y;
    • 数据迁移完成,修改数据库服务配置,原来x个库的配置升级为y个库的配置
    • 重启服务,连接新库重新对外提供服务
    • 站点发布一个公告,例如:“为了为广大用户提供更好的服务,本站点将在今晚00:00-2:00之间升 级,给您带来不便抱歉”;
    • 时间到了,停止所有对外服务;
  • 平滑扩容数据库扩容的过程中,如果想要持续对外提供服务,保证服务的可用性,平滑扩容方案是最好的选择。 平滑扩容就是将数据库数量扩容成原来的2倍,比如:由2个数据库扩容到4个数据库,具体步骤如下
    • 新增2个数据库
    • 配置双主进行数据同步(先测试、后上线)
    • 数据同步完成之后,配置双主双写(同步因为有延迟,如果时时刻刻都有写和更新操作,会存在不 准确问题)
    • 数据同步完成后,删除双主同步,修改数据库配置,并重启
    • 此时已经扩容完成,但此时的数据并没有减少,新增的数据库跟旧的数据库一样多的数据,此时还 需要写一个程序,清空数据库中多余的数据,如: User1去除 uid % 4 = 2的数据; User3去除 uid % 4 = 0的数据; User2去除 uid % 4 = 3的数据; User4去除 uid % 4 = 1的数据;
    • 平滑扩容方案能够实现n库扩2n库的平滑扩容,增加数据库服务能力,降低单库一半的数据量。其核心 原理是:成倍扩容,避免数据迁移。
    • 优点: 扩容期间,服务正常进行,保证高可用 相对停机扩容,时间长,项目组压力没那么大,出错率低 扩容期间遇到问题,随时解决,不怕影响线上服务 可以将每个数据库数据量减少一半
    • 缺点: 程序复杂、配置双主同步、双主双写、检测数据同步等 后期数据库扩容,比如成千上万,代价比较高

参考文档:https://www.cnblogs.com/tkzL/p/8708671.html