极客时间-Java 性能调优实战读书笔记

767 阅读10分钟

1. 背景

主要分为多线程高并发、JVM、数据库、代码设计模式来做项目的调优,从而达到更少的CPU和内存来抗住更多的接口访问

2. 数据库性能调优

慢 SQL 语句的几种常见诱因

  1. 无索引、索引失效导致慢查询

  2. 锁等待

  3. 不恰当的 SQL 语句

行锁是基于索引加的锁,如果我们在更新操作时,条件索引失效,那么行锁也会升级为表锁。

优化 SQL 语句的步骤

  1. 通过 EXPLAIN 分析 SQL 执行计划

  2. 通过 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 列设置为唯一索引列。虽然不能防止幻读, 但我们可以利用它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会抛出异常。

解决死锁的最佳方式当然就是预防死锁的发生了,我们平时编程中,可以通过以下一些常规手段来预防死锁的发生:

  1. 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
  1. 在允许幻读和不可重复读的情况下,尽量使用 RC 事务隔离级别,可以避免 gap lock 导致的死锁问题;
  1. 更新表时,尽量使用主键更新;
  1. 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
  1. 设置锁等待超时参数,我们可以通过 innodb_lock_wait_timeout 设置合理的等待超时阈值,特别是在一些高并发的业务中, 我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。

MySQL默认开启了死锁检测机制,当检测到死锁后会选择一个最小(锁定资源最少得事务)的事务进行回滚

36 | 记一次线上SQL死锁事故:如何避免死锁?

这一篇还得好好看看

由于订单表用户查询比较多,此时我们应该考虑使用用户 ID 字段做 Hash 取模,对订单表进行水平分表。如果需要考虑高并发时的订单处理能力, 我们可以考虑基于用户 ID 字段 Hash 取模实现分库分表。这也是大部分公司对订单表分库分表的处理方式。

分表分库之后面临的问题

  1. 分布式事务问题 通常,我们解决分布式事务有两种通用的方式:两阶事务提交(2PC)以及补偿事务提交(TCC)。有关分布式事务的内容,我将在第 41 讲中详细介绍。
  1. 跨节点 JOIN 查询问题 通常,我们会冗余表或冗余字段来优化跨库 JOIN 查询。对于一些基础表,例如商品信息表,我们可以在每一个订单分库中复制一张基础表, 避免跨库 JOIN 查询。而对于一两个字段的查询,我们也可以将少量字段冗余在表中,从而避免 JOIN 查询,也就避免了跨库 JOIN 查询。
  1. 跨节点分页查询问题 通常我们建议使用两套数据来解决跨节点分页查询问题,一套是基于分库分表的用户单条或多条查询数据,一套则是基于 Elasticsearch、Solr 存储的订单数据, 主要用于运营人员根据其它字段进行分页查询。为了不影响提交订单的业务性能,我们一般使用异步消息来实现 Elasticsearch、Solr 订单数据的新增和修改。
  1. 全局主键 ID 问题 Redis 分布式锁实现一个递增的主键 ID
  2. 扩容问题 我们在最开始设计表数据量时,尽量使用 2 的倍数来设置表数量。当我们需要扩容时,也同样按照 2 的倍数来扩容,这种方式可以减少数据的迁移量。

数据库的分区和分表的区别

都是解决数据量大查询效率慢的问题,对一张表做分区,表面上还是一张表,只不过数据保存在不同的位置,对数据进行读取,操作的表名称不变,数据库会自己去组织各个分区的数据 对于INnoDB的存储引擎,有一个用于存储表结构的.frm文件,有一个存储表数据的.idb文件,做了分区以后,还是只有一个.frm文件但是.idb 还是会有多个 但是分表会有多个.frm文件和.idb文件,这就是分区分表存储上的区别,数据量大的时候先考虑分区,分区搞不定在考虑分表

分表之后,单表数据量变小,页缓存率高了,IO读写性能更优了,分表也能降低锁带来的阻塞,提高事务的处理效率,小的表也可以提高备份和恢复的速度

可以基于一个公共表来存储商品的公共信息,同时结合搜索引擎,将商品详细信息存储到键值对数据库,例如 ElasticSearch、Solr 中。 在用户没有登录系统的情况下,我们是通过 cookie 来保存购物车的商品信息,而在用户登录系统之后,购物车的信息会保存到数据库中。 ,可以将购物车中近一个月的商品信息都存放到 Redis 中,且至少为一个分页的信息。 通常我们是通过计算用户 ID 字段的 Hash 值来实现订单的分表,这种方式可以优化用户购买端对订单的操作性能。如果我们需要对订单表进行水平分库,那就还是基于用户 ID 字段来实现。

而对于分页查询,通常我们建议冗余订单信息到大数据中。后台管理系统通过大数据来查询订单信息,用户在提交订单并且付款之后,后台将会同步这条订单到大数据。 用户在 C 端修改或运营人员在后台修改订单时,会通过异步方式通知大数据更新该订单数据,这种方式可以解决分表分库后带来的分页查询问题。

电商系统实战练习了如何进行表设计,可以总结为以下几个要点:

  1. 在字段比较复杂、易变动、不方便统一的情况下,建议使用键值对来代替关系数据库表存储;

  2. 在高并发情况下的查询操作,可以使用缓存代替数据库操作,提高并发性能;

  3. 数据量叠加比较快的表,需要考虑水平分表或分库,避免单表操作的性能瓶颈;

  4. 除此之外,我们应该通过一些优化,尽量避免比较复杂的 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 行记录。

3. JVM调优

4. 多线程高并发调优