1. 背景
主要分为多线程高并发、JVM、数据库、代码设计模式来做项目的调优,从而达到更少的CPU和内存来抗住更多的接口访问
2. 数据库性能调优
慢 SQL 语句的几种常见诱因
无索引、索引失效导致慢查询
锁等待
不恰当的 SQL 语句
行锁是基于索引加的锁,如果我们在更新操作时,条件索引失效,那么行锁也会升级为表锁。
优化 SQL 语句的步骤
通过 EXPLAIN 分析 SQL 执行计划
通过 Show Profile 分析 SQL 执行性能
一般来说,得保证查询至少达到 range 级别,最好能达到 ref。
select @@have_profiling;
show profiles;
show profile for query 1198;
我们知道,InnoDB 既实现了行锁,也实现了表锁。行锁是通过索引实现的,如果不通过索引条件检索数据,那么 InnoDB 将对表中所有的记录进行加锁,其实就是升级为表锁了。
行锁的具体实现算法有三种:record lock、gap lock 以及 next-key lock。record lock 是专门对索引项加锁;gap lock 是对索引项之间的间隙加锁; next-key lock 则是前面两种的组合,对索引项以其之间的间隙加锁。
聚簇索引中的叶子节点则记录了主键值、事务 id、用于事务和 MVCC 的回流指针以及所有的剩余列
行锁的具体实现算法有三种:record lock、gap lock 以及 next-key lock。record lock 是专门对索引项加锁;gap lock 是对索引项之间的间隙加锁;
next-key lock 则是前面两种的组合,对索引项以其之间的间隙加锁。
避免死锁的措施
避免死锁最直观的方法就是在两个事务相互等待时,当一个事务的等待时间超过设置的某一阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。 这种方法简单有效,在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的。
另外,我们还可以将 order_no 列设置为唯一索引列。虽然不能防止幻读, 但我们可以利用它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会抛出异常。
解决死锁的最佳方式当然就是预防死锁的发生了,我们平时编程中,可以通过以下一些常规手段来预防死锁的发生:
- 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
- 在允许幻读和不可重复读的情况下,尽量使用 RC 事务隔离级别,可以避免 gap lock 导致的死锁问题;
- 更新表时,尽量使用主键更新;
- 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
- 设置锁等待超时参数,我们可以通过 innodb_lock_wait_timeout 设置合理的等待超时阈值,特别是在一些高并发的业务中, 我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。
MySQL默认开启了死锁检测机制,当检测到死锁后会选择一个最小(锁定资源最少得事务)的事务进行回滚
36 | 记一次线上SQL死锁事故:如何避免死锁?
这一篇还得好好看看
由于订单表用户查询比较多,此时我们应该考虑使用用户 ID 字段做 Hash 取模,对订单表进行水平分表。如果需要考虑高并发时的订单处理能力, 我们可以考虑基于用户 ID 字段 Hash 取模实现分库分表。这也是大部分公司对订单表分库分表的处理方式。
分表分库之后面临的问题
- 分布式事务问题 通常,我们解决分布式事务有两种通用的方式:两阶事务提交(2PC)以及补偿事务提交(TCC)。有关分布式事务的内容,我将在第 41 讲中详细介绍。
- 跨节点 JOIN 查询问题 通常,我们会冗余表或冗余字段来优化跨库 JOIN 查询。对于一些基础表,例如商品信息表,我们可以在每一个订单分库中复制一张基础表, 避免跨库 JOIN 查询。而对于一两个字段的查询,我们也可以将少量字段冗余在表中,从而避免 JOIN 查询,也就避免了跨库 JOIN 查询。
- 跨节点分页查询问题 通常我们建议使用两套数据来解决跨节点分页查询问题,一套是基于分库分表的用户单条或多条查询数据,一套则是基于 Elasticsearch、Solr 存储的订单数据, 主要用于运营人员根据其它字段进行分页查询。为了不影响提交订单的业务性能,我们一般使用异步消息来实现 Elasticsearch、Solr 订单数据的新增和修改。
- 全局主键 ID 问题 Redis 分布式锁实现一个递增的主键 ID
- 扩容问题 我们在最开始设计表数据量时,尽量使用 2 的倍数来设置表数量。当我们需要扩容时,也同样按照 2 的倍数来扩容,这种方式可以减少数据的迁移量。
数据库的分区和分表的区别
都是解决数据量大查询效率慢的问题,对一张表做分区,表面上还是一张表,只不过数据保存在不同的位置,对数据进行读取,操作的表名称不变,数据库会自己去组织各个分区的数据 对于INnoDB的存储引擎,有一个用于存储表结构的.frm文件,有一个存储表数据的.idb文件,做了分区以后,还是只有一个.frm文件但是.idb 还是会有多个 但是分表会有多个.frm文件和.idb文件,这就是分区分表存储上的区别,数据量大的时候先考虑分区,分区搞不定在考虑分表
分表之后,单表数据量变小,页缓存率高了,IO读写性能更优了,分表也能降低锁带来的阻塞,提高事务的处理效率,小的表也可以提高备份和恢复的速度
可以基于一个公共表来存储商品的公共信息,同时结合搜索引擎,将商品详细信息存储到键值对数据库,例如 ElasticSearch、Solr 中。 在用户没有登录系统的情况下,我们是通过 cookie 来保存购物车的商品信息,而在用户登录系统之后,购物车的信息会保存到数据库中。 ,可以将购物车中近一个月的商品信息都存放到 Redis 中,且至少为一个分页的信息。 通常我们是通过计算用户 ID 字段的 Hash 值来实现订单的分表,这种方式可以优化用户购买端对订单的操作性能。如果我们需要对订单表进行水平分库,那就还是基于用户 ID 字段来实现。
而对于分页查询,通常我们建议冗余订单信息到大数据中。后台管理系统通过大数据来查询订单信息,用户在提交订单并且付款之后,后台将会同步这条订单到大数据。 用户在 C 端修改或运营人员在后台修改订单时,会通过异步方式通知大数据更新该订单数据,这种方式可以解决分表分库后带来的分页查询问题。
电商系统实战练习了如何进行表设计,可以总结为以下几个要点:
在字段比较复杂、易变动、不方便统一的情况下,建议使用键值对来代替关系数据库表存储;
在高并发情况下的查询操作,可以使用缓存代替数据库操作,提高并发性能;
数据量叠加比较快的表,需要考虑水平分表或分库,避免单表操作的性能瓶颈;
除此之外,我们应该通过一些优化,尽量避免比较复杂的 JOIN 查询操作,例如冗余一些字段,减少 JOIN 查询;创建一些中间表,减少 JOIN 查询。
InnoDB Buffer Pool 它不仅存储了表索引块,还存储了表数据 innodb_buffer_pool_size 可以通过参数 innodb_buffer_pool_size 来设置 IBP 的大小,IBP 设置得越大,InnoDB 表性能就越好。但是,将 IBP 大小设置得过大也不好,可能会导致系统发生 SWAP 页交换。 所以我们需要在 IBP 大小和其它系统服务所需内存大小之间取得平衡。MySQL 推荐配置 IBP 的大小为服务器物理内存的 80%。
innodb_buffer_pool_instances innodb_buffer_pool_size 大小超过 1GB,innodb_buffer_pool_instances 值就默认为 8;否则,默认为 1。 ( innodb_read_io_threads + innodb_write_io_threads ) = innodb_buffe_pool_instances 如果读大于写,我们应该考虑将读线程的数量设置得大一些,写线程数量小一些;否则,反之。
innodb_log_file_size 理论上来说,innodb_log_file_size 设置得越大,缓冲池中需要的检查点刷新活动就越少,从而节省磁盘 I/O。那是不是将这个日志文件设置得越大越好呢? 如果日志文件设置得太大,恢复时间就会变长,这样不便于 DBA 管理。在大多数情况下,我们将日志文件大小设置为 1GB 就足够了。
innodb_log_buffer_size
InnoDB 主要包括了内存池、后台线程以及存储文件。内存池又是由多个内存块组成的,主要包括缓存磁盘数据、redo log 缓冲等; 后台线程则包括了 Master Thread、IO Thread 以及 Purge Thread 等; 由 InnoDB 存储引擎实现的表的存储结构文件一般包括表结构文件(.frm)、共享表空间文件(ibdata1)、独占表空间文件(ibd)以及日志文件(redo 文件等)等
后台线程 Master Thread 主要负责将缓冲池中的数据异步刷新到磁盘中,除此之外还包括插入缓存、undo 页的回收等,IO Thread 是负责读写 IO 的线程,而 Purge Thread 主要用于回收事务已经提交了的 undo log, Pager Cleaner Thread 是新引入的一个用于协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞。
存储文件 在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
InnoDB 逻辑存储结构 InnoDB 逻辑存储结构分为表空间(Tablespace)、段 (Segment)、区 (Extent)、页 Page) 以及行 (row)。 表空间是由各个段组成的,段一般分为数据段、索引段和回滚段等。我们知道,InnoDB 默认是基于 B + 树实现的数据存储。 这里的索引段则是指的 B + 树的非叶子节点,而数据段则是 B + 树的叶子节点。而回滚段则指的是回滚数据, 之前我们在讲事务隔离的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。
区是表空间的单元结构,每个区的大小为 1MB。而页是组成区的最小单元,页也是 InnoDB 存储引擎磁盘管理的最小单元, 每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。
InnoDB 存储引擎是面向行的(row-oriented),也就是说数据是按行进行存放的,每个页存放的行记录也是有硬性定义的, 最多允许存放 16KB/2-200 行,即 7992 行记录。