RE:MySQL

MySQL常用引擎

  • MyISAM:MySQL 5.5之前的默认引擎,不支持行锁、事务、外键,非聚簇索引。

  • InnoDB:MySQL 5.5之后的默认引擎,支持行锁、事务、外键,聚簇索引。

一般来说,MyISAM适合读密集型的表,InnoDB适合写密集型的表。

字符集与校对规则

字符集(charset):二级制编码到指定字符的映射。

校对规则(collate):当前字符集下的排序规则。

UTF8是我们常用的字符集,它的常用校对规则如下:

  • utf8_general_ci:UTF8的默认校对规则,不支持扩展,仅在字符之间逐个比较,速度很快。

  • utf8_unicode_ci:依据Unicode的算法(UCA)执行,支持扩展,即可以将一个字母看作与其它字母组合相等,准确性比utf8_general_ci高。

  • utf8_bin:以二进制数据编译存储每个字符。

一般情况下选择utf8_general_ci即可。

UTF8MB4是MySQL在5.5.3之后增加的编码,MB4指Most Bytes 4,专门用来兼容4字节的Unicode。作为UTF8的超集,UTF8MB4有更好的兼容性。

索引

MySQL索引使用的数据结构主要有BTree索引和哈希索引:

  • 哈希索引:底层是哈希表,作为单条记录查询时性能很快。

  • BTree索引:多数场景下的索引选择,底层是BTree中的B+Tree,两种引擎对其的实现方式不一样:

    • MyISAM:非聚簇索引,B+Tree的叶节点存放数据的地址,检索时如果指定key存在,先取出地址然后按地址读取数据记录。

    • InnoDB:聚簇索引,数据文件本身就是索引文件,B+Tree的叶节点存放了完整的数据记录,主索引的key也就是数据表的主键,而其他的辅助索引会存储相应记录主键的值而不是地址。检索时,如果依据主索引找到key即可直接读取数据,如果依据辅助索引查找,则需要先取出主键值再走一遍主索引。

应用索引时的一些注意事项:

  • 最左前缀原则:查询条件精确匹配联合索引的左边一列或连续多列。

  • 避免对WHERE字句中的字段附加函数,否则无法命中索引。

  • 主键应该是与业务无关的自增字段,否则可能导致索引混乱。

  • 利用索引覆盖:查询条件命中索引的同时查询字段也属于索引中的字段。

  • 避免冗余索引:尽量扩展已有索引而不是创建新索引,否则可能导致索引的功能相同即索引冗余。

  • 附加索引的列应该设置为NOT NULL,否则索引会被放弃而进行全表扫描。

  • 删除长期未使用的索引,减少不必要的性能消耗。

  • 联合查询时ON或USING中的列应附加索引,GROUP BY或ORDER BY中的列应只涉及一个表。

  • 查询条件的字段应使用正确的数据类型,否则MySQL的自动类型转换会导致无法命中索引。

索引虽然加快了查询速度,但是也会消耗更多的存储空间与维护资源,同时影响INSERT、DELETE的速度。索引并不是越多越好,如果表数据较少或者重复数据较多时不建议建立索引。

事务机制

事务特性:

  • A(Atomic)原子性:一个事务只会全部成功或全部失败。

  • C(Consistency)一致性:事务的运行不会改变数据的一致性。

  • I(Isolation)独立性:并发的事务之间不会互相影响。

  • D(Durability)持久性:事务提交后,该事务对数据所作的修改永久地保存在数据库中。

事务隔离级别:

  • Read Uncommitted:可能出现脏读、幻读、不可重复读,实际情况下不会用到。

  • Read Committed:可能出现幻读、不可重复读,JDBC的默认事务级别,同时

  • Repeatable Read:可能出现幻读,MySQL的默认事务级别。

  • Serializable:不会出现任何并发问题,但事务并发性急剧下降。

MySQL5.0之前,因为主从复制不一致性的问题,MySQL选择了Repeatable Read作为默认的隔离级别,但是在互联网项目中,一般会将隔离级别设置为Read Committed,这样可以减少死锁的出现几率,提高并发性。

事务隔离的实现基于锁机制与并发调度:

  • 表级锁:对当前操作的表加锁,简单快速,不会出现死锁,但是并发度低。

  • 行级锁:对当前操作的行加锁,锁粒度小,并发度高,但是消耗资源更多,会出现死锁。

虽然行级锁的粒度小、并发度高,但在有些情况下,如表数据非常多或事务比较复杂,应用表级锁的效率会更高。

锁机制可进一步划分为共享锁和排他锁:

  • 共享锁(S):又被称为读锁,其他用户可再加共享锁读取数据,但是获取共享锁的事务只可读取而不可修改数据。

  • 排他锁(X):又被称为写锁,一个事务对数据加上排他锁后,其他事务不可再对此数据加任何形式的锁。获取排他锁的事务既可读取又可修改数据。

表数据过多时的优化方案

  • 限制数据范围:限制查询语句的数据范围,例如查询订单记录时,只在最近一个月的数据中筛选。

  • 读/写分离:经典的数据库拆分方案,主库写、从库读。

  • 缓存:使用MySQL的查询缓存或者建立缓存层。

  • 垂直分区、水平分区:按照一定规则拆分数据或者应用数据库中间件等。

高可用方案

  • MHA,Master High Availablity,一主多从模式,也是最常用的方案。管理节点会定时探测集群中的Master节点,如果出现故障,自动将存储最新数据的Slave提升为新的Master,然后将其他的Slave指向新的Master。

  • MMM,Multi-Master Replication Manager,双主模式。同一时刻只有一个主允许写,另一个主允许读,一个主挂掉,其下面的Slave同样挂掉。因为存在数据不一致问题,此方案适合数据一致性要求不高的业务。

主从模式中,经常遇到的一个问题是Slave数据滞后于Master,一个解决方案是MySQL5.5之后引入的半同步复制机制,MySQL客户端请求时阻塞,直到数据至少已同步到一个Slave或者超时,这样可在一定程度上提高数据的一致性。

DDL注意事项

  • 禁用存储过程、函数、触发器、外键约束,让它们尽量依赖于业务层面的处理,这样可以具有良好的扩展性。

  • 非(!=)查询不会匹配值为NULL的列,例如 where status != ‘FINISH‘ 不会挑选出status=NULL的记录。

  • 尽量使用枚举或整数类型代替字符串类型。

  • ID使用BIGINT即可,对应Java的Long类型。

  • 整数类型指定宽度并不影响存储大小,例如指定INT(11)后,INT依然有32位存储空间,宽度仅表示显示长度。

  • 避免使用DECIMAL和浮点数,可以用BIGINT代替,即浮点数乘以一个倍数。

  • 表中最好带有创建和更新时间戳,以及创建或修改的用户标识,这是为了更好地审计和追踪数据。

  • 不要真的删除数据,可以给它们设置删除标记或者做版本化处理。

  • 单表不要设置太多字段。

  • 用整数类型而不是字符串存储IP。

DML注意事项

  • 查询语句不要使用全属性选择器(*)。

  • 尽量避免 "!"、"<>"、"or" 查询,而使用 "in" 代替,"in" 的查询效率是log(n)。

  • 模糊匹配时,只有后缀模糊查询才可以命中索引,如like ‘a%‘。

  • 查询记录数目时,count(1)与count(*)等价,都会在有索引时选择合适的索引,没有索引时进行全表扫描。count(column)只统计column不为NULL的记录条数。

  • 避免多于两表的Join,尽量使用冗余策略解决联表问题。

  • 避免列运算。

  • 以批量插入代替循环插入,减少网络I/O开销。

  • 避免过长的SQL语句,可以拆成多个小的语句,以减少锁的时间。

  • 使用UNION时,MySQL会给临时表加上DISTINCT唯一性检查,如果不需要去重处理,应该使用UNION ALL。

  • 在主键自增的表中插入一条记录后,可以用SELECT LAST_INSERT_ID() 获取这个自增值。

  • 查询数据为bit类型时,需要加0才可以看到其值,如select bit_column+0 from test,显示为十进制数。

数据库连接池

数据库连接池避免了频繁创建、销毁连接带来的性能开销,常见的连接池有以下几个:

  • HikariCP:功能简单,性能优秀,起源于BoneCP。

  • Druid:alibaba开源的数据库连接池,有监控、统计功能,性能良好。

  • DBCP:commons-pool之上的封装。

  • C3P0:历史久远,逻辑复杂。

参考资料

《Java工程师修炼之道》