想成为一名顶尖Java开发工程师?这些优化手段一定要掌握!(三)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 想成为一名顶尖Java开发工程师?这些优化手段一定要掌握!

🍊 异常发现处理

在使用MySQL时,可能会遇到各种异常情况,例如连接错误、查询错误、数据删除错误等等。在处理这些异常情况时,开发人员需要了解异常的原因和处理方法,以便及时排除问题,保障系统的稳定性和可靠性。

🎉 数据库监控

及时将数据库异常通过短信、邮件、微信等形式通知给管理员,并且可以将数据库运行的实时指标统计分析图表显示出来,便于更好地对数据库进行规划和评估,目前市面上比较主流的数据库监控工具有Prometheus + Grafana + mysqld_exporter(比较受欢迎)、SolarWinds SQL Sentry、Database Performance Analyzer、OpenFalcon。

🎉 数据库日志

在MySQL中,有一些关键的日志可以用作异常发现并通过这些日志给出解决方案:

(1)重做日志(redo log):记录物理级别的页修改操作,例如页号123、偏移量456写入了“789”数据。可以通过“show global variables like ‘innodb_log%’;”命令查看。主要用于事务提交时保证事务的持久性和回滚。

(2)回滚日志(undo log):记录逻辑操作日志,例如添加一条记录时会记录一条相反的删除操作。可以通过“show variables like ‘innodb_undo%’;”命令查看。主要用于保证事务的原子性,在需要时回滚事务。

(3)变更日志/二进制日志(bin log):记录数据库执行的数据定义语句(DDL)和数据操作语句(DML)等操作。例如数据库意外挂机时,可以通过二进制日志文件查看用户执行的命令,并根据这些操作指令恢复数据库或将数据复制到其他数据库中。可以通过“show variables like ‘%log_bin%’;”命令查看。主要用于性能优化和复制数据。

(4)慢查询日志:记录响应时间超过指定阈值的SQL语句。主要用于性能优化。可以通过“show variables like ‘%slow_query_log%’;”命令查看。

(5)错误日志:记录MySQL服务启动、运行、停止时的诊断信息、错误信息和警告提示。主要用于排查MySQL服务出现异常的原因。可以通过“SHOW VARIABLES LIKE ‘log_err%’;”命令查看。

(6)通用查询日志:记录用户的所有操作,无论是所有的SQL语句还是调整MySQL参数或者启动和关闭MySQL都会记录。可以还原操作的场景。通过SHOW VARIABLES LIKE ‘%general%’;命令查看。

(7)中继日志(relay log):只存在主从数据库的从数据库上,用于主从同步,可以在xx-relaybin.index索引文件和-relaybin.0000x数据文件查看。

(8)数据定义语句日志(ddl.log):记录数据定义的SQL,比如ALTER TABLE。

(9)processlist日志:查看正在执行的sql语句。

(10) innodb status日志:查看事务、锁、缓冲池和日志文件,主要用于诊断数据库性能。

🎉 数据库巡检

巡检工作保障系统平稳有效运行,比如飞机起飞巡检保证起飞后能够正常工作。巡检工作主要由数据库管理员和后端开发工程师负责。

数据库管理员主要负责处理数据库基础功能/高可用/备份/中间件/报警组件、集群拓扑、核心参数等集群层面的隐患、服务器硬件层面隐患,对于磁盘可用空间预测等范围。

后端开发工程师主要负责库表设计缺陷、数据库使用不规范等引起的业务故障或性能问题的隐患,定期采集整型字段值有没有超过最大值,因为整型类型的字段保存的数值有上限。对于读写情况需要定期观察表大小,找出有问题的大表进行优化调整。

🎉 资源评估

测试人员进行压测,观察极限环境下数据库各项指标是否正常工作,运维工程师或者数据库管理员对数据容量进行评估,服务器资源需要提前规划,同时设置预警通知,超过阈值安排相关人员进行扩容,从而保证数据库稳定运行。

🍊 数据服务

数据服务的主要目的是帮助用户规划和迁移数据,备份和恢复数据库以及进行数据校验等功能,以确保用户的数据始终处于安全可靠的状态。

🎉 子表结构生成

一个表进行拆分,会根据业务实际情况进行拆解,例如用户表可以根据地区拆分tb_user可拆分成上海地区的用户表(tb_user_sh)、广州地区的用户表(tb_user_gz),那么全国有很多个城市,每个地方都需要创建一张子表并且维护它会比较费时费力,通常情况下,会开发3个接口做表结构同步:根据主表创建子表、主表字段同步到子表、主表索引同步子表。

下面对这3个接口提供思路以及关键代码。

主表创建子表,代码如下:

/**
* {
*     "tableName": "tb_user",
*     "labCodes": [
*         "sh",//上海
*         "gz"//广州
*     ]
* }     
*/
public Boolean createTable(ConfigReq reqObject) {
  if (CollectionUtils.isEmpty(reqObject.getLabCodes())) {
    return false;
  }
  List<String> labCodes = reqObject.getLabCodes();
  for (String labCode: labCodes){
    //主表表名
    String tableName = reqObject.getTableName();
    //子表后表名
    String newTable = String.format("%s_%s", tableName, labCode);
    //校验子表是否存在
    Integer checkMatrix = configExtMapper.checkTable(newTable);
    if(checkMatrix == null || checkMatrix.intValue() < 0){
    //创建子表结构
    configExtMapper.createConfigTable(tableName, newTable);
    }
    }
  return true;
}

主表字段同步到子表,代码如下:

/**
* 主表字段同步到子表
* @param masterTable 主表
* @return
*/
private Boolean syncAlterTableColumn(String masterTable) {
  String table = masterTable + "%";
  //获取子表名
  List<String> tables = configExtMapper.getTableInfoList(table);
  if(CollectionUtils.isEmpty(tables)){
    return false;
  }
  //获取主表结构列信息
  List<ColumnInfo> masterColumns = configExtMapper.getColumnInfoList(masterTable);
  if (masterColumns.isEmpty()){
    return false;
  }
  String alterName = null;
  for (ColumnInfo column: masterColumns) {
    column.setAlterName(alterName);
    alterName = column.getColumnName();
  }
  for(String tableName : tables){
    if(StringUtils.equalsIgnoreCase(tableName, masterTable)){
      continue;
    }
    //获取子表结构列信息
    List<ColumnInfo> columns = configExtMapper.getColumnInfoList(tableName);
    if(CollectionUtils.isEmpty(columns)){
      continue;
    }
    for (ColumnInfo masterColumn : masterColumns) {
      ColumnInfo column = columns.stream().filter(c -> StringUtils.equalsIgnoreCase(c.getColumnName(),
      masterColumn.getColumnName())).findFirst().orElse(null);
      if (column == null){
        column = new ColumnInfo();
        column.setColumnName(masterColumn.getColumnName());//列名
        column.setAddColumn(true);//是否修改
      }
      if (column.hashCode() == masterColumn.hashCode()){
        continue;
      }
      column.setTableName(tableName);//表名
      column.setColumnDef(masterColumn.getColumnDef());//是否默认值
      column.setIsNull(masterColumn.getIsNull());//是否允许为空(NO:不能为空、YES:允许为空)
      column.setColumnType(masterColumn.getColumnType());//字段类型(如:varchar(512)、text、bigint(20)、datetime)
      column.setComment(masterColumn.getComment());//字段备注(如:备注)
      column.setAlterName(masterColumn.getAlterName());//修改的列名
      //创建子表字段
      configExtMapper.alterTableColumn(column);
    }
  }
  return true;
}

主表索引同步子表,代码如下:

/**
* 主表索引同步子表
* @param masterTableName 主表名
* @return
*/
private Boolean syncAlterConfigIndex(String masterTableName) {
  String table = masterTableName + "%";
  //获取子表名
  List<String> tableInfoList = configExtMapper.getTableInfoList(table);
  if (tableInfoList.isEmpty()){
    return false;
  }
  // 获取所有索引
  List<String> allIndexFromTableName = configExtMapper.getAllIndexNameFromTableName(masterTableName);
  if (CollectionUtils.isEmpty(allIndexFromTableName)) {
    return false;
  }
  for (String indexName : allIndexFromTableName) {
    //获取拥有索引的列名
    List<String> indexFromIndexName = configExtMapper.getAllIndexFromTableName(masterTableName, indexName);
    for (String tableName : tableInfoList) {
      if (!tableName.startsWith(masterTableName)) {
        continue;
      }
      //获取索引名称
      List<String> addIndex = configExtMapper.findIndexFromTableName(tableName, indexName);
      if (CollectionUtils.isEmpty(addIndex)) {
        //创建子表索引
        configExtMapper.commonCreatIndex(tableName, indexName, indexFromIndexName);
      }
    }
  }
  return true;
}

子表结构生成的SQL,代码如下:

<!--校验子表是否存在 这里db_user写死了数据库名称,后面可以根据实际情况调整-->
<select id="checkTable" resultType="java.lang.Integer" >
  SELECT 1 FROM INFORMATION_SCHEMA.`TABLES` WHERE TABLE_SCHEMA = 'db_user' AND TABLE_NAME = #{tableName};
</select>
<!--创建子表结构-->
<update id="createConfigTable" >
  CREATE TABLE `${newTableName}` LIKE `${sourceName}`;
</update>
<!--获取子表名-->
<select id="getTableInfoList" resultType="java.lang.String">
  SELECT `TABLE_NAME`
  FROM INFORMATION_SCHEMA.`TABLES`
  WHERE `TABLE_NAME` LIKE #{tableName};
</select>
<!--获取主/子表结构列信息 这里db_user写死了数据库名称,后面可以根据实际情况调整-->
<select id="getColumnInfoList" resultType="com.yunxi.datascript.config.ColumnInfo">
  SELECT `COLUMN_NAME` AS columnName
  ,COLUMN_DEFAULT AS columnDef   -- 是否默认值
  ,IS_NULLABLE AS isNull    -- 是否允许为空
  ,COLUMN_TYPE AS columnType    -- 字段类型
  ,COLUMN_COMMENT AS comment      -- 字段备注
  FROM INFORMATION_SCHEMA.`COLUMNS`
  WHERE TABLE_SCHEMA = 'db_user'
  AND `TABLE_NAME` = #{tableName}
  ORDER BY ORDINAL_POSITION ASC;
</select>
<!--创建子表字段-->
<update id="alterTableColumn" parameterType="com.yunxi.datascript.config.ColumnInfo">
  ALTER TABLE `${tableName}`
  <choose>
    <when test="addColumn">
      ADD COLUMN
    </when >
    <otherwise>
      MODIFY COLUMN
    </otherwise>
  </choose>
  ${columnName}
  ${columnType}
  <choose>
    <when test="isNull != null and isNull == 'NO'">
      NOT NULL
    </when >
    <otherwise>
      NULL
    </otherwise>
  </choose>
  <if test="columnDef != null and columnDef != ''">
    DEFAULT #{columnDef}
  </if>
  <if test="comment != null and comment != ''">
    COMMENT #{comment}
  </if>
  <if test="alterName != null and alterName != ''">
    AFTER ${alterName}
  </if>
</update>
<!--获取所有索引-->
<select id="getAllIndexNameFromTableName" resultType="java.lang.String">
  SELECT DISTINCT index_name FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name != 'PRIMARY'
</select>
<!--获取拥有索引的列名-->
<select id="getAllIndexFromTableName" resultType="java.lang.String">
  SELECT COLUMN_NAME FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name = #{idxName} AND index_name != 'PRIMARY'
</select>
<!--获取索引名称-->
<select id="findIndexFromTableName" resultType="java.lang.String">
  SELECT index_name FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name = #{idxName}
</select>
<!--创建子表索引-->
<update id="commonCreatIndex">
  CREATE INDEX ${idxName} ON `${tableName}`
  <foreach collection="list" item="item" open="(" close=")" separator=",">
    `${item}`
  </foreach>;
</update>

根据以上关键代码以及实现思路结合实际情况开发出3个接口足以满足日常分表需求了。

🎉 数据迁移

数据迁移通常有两种情况:

第一种是开发人员编码,将数据从一个数据库读取出来,再将数据异步的分批次批量插入另一个库中。

第二种是通过数据库迁移工具,通常使用Navicat for MySQL就可以实现数据迁移。

数据迁移需要注意的是不同数据库语法和实现不同,数据库版本不同,分库分表时数据库的自增主键ID容易出现重复键的问题,通常情况下会在最初需要自增时考虑分布式主键生成策略。

🎉 数据校验

数据校验有对前端传入的参数进行数据校验、有程序插入数据库中的数据进行校验,比如非空校验、长度校验、类型校验、值的范围校验等、有对数据迁移的源数据库和目标数据库的表数据进行对比、这些都是保证数据的完整性。

🍊 读写分离

MySQL读写分离是数据库优化的一种手段,通过将读和写操作分离到不同的数据库服务器上,可以提高数据库的读写性能和负载能力。

🎉 主从数据同步

业务应用发起写请求,将数据写到主库,主库将数据进行同步,同步地复制数据到从库,当主从同步完成后才返回,这个过程需要等待,所以写请求会导致延迟,降低吞吐量,业务应用的数据读从库,这样主从同步完成就能读到最新数据。

🎉 中间件路由

业务应用发起写请求,中间件将数据发往主库,同时记录写请求的key(例如操作表加主键)。当业务应用有读请求过来时,如果key存在,暂时路由到主库,从主库读取数据,在一定时间过后,中间件认为主从同步完成,就会删除这个key,后续读将会读从库。

🎉 缓存路由

缓存路由和中间件路由类似,业务应用发起写请求,数据发往主库,同时缓存记录操作的key,设置缓存的失效时间为主从复制完成的延时时间。如果key存在,暂时路由到主库。如果key不存在,近期没发生写操作,暂时路由到从库。

🌟 Redis调优

🍊 绑定CPU内核

现代计算机的CPU都是多核心多线程,例如i9-12900k有16个内核、24个逻辑处理器、L1缓存1.4MB、L2缓存14MB、L3缓存30MB,一个内核下的逻辑处理器共用L1和L2缓存。

Redis的主线程处理客户端请求、子进程进行数据持久化、子线程处理RDB/AOF rewrite、后台线程处理异步lazy-free和异步释放fd等。这些线程在多个逻辑处理器之间切换,所以为了降低Redis服务端在多个CPU内核上下文切换带来的性能损耗,Redis6.0版本提供了进程绑定CPU 的方式提高性能。

在Redis6.0版本的redis.conf文件配置即可:

  • server_cpulist:RedisServer和IO线程绑定到CPU内核
  • bio_cpulist:后台子线程绑定到CPU内核
  • aof_rewrite_cpulist:后台AOF rewrite进程绑定到CPU内核
  • bgsave_cpulist:后台RDB进程绑定到CPU内核

🍊 使用复杂度过高的命令

Redis有些命令复制度很高,复杂度过高的命令如下:

MSET、MSETNX、MGET、LPUSH、RPUSH、LRANGE、LINDEX、LSET、LINSERT、HDEL、HGETALL、HKEYS/HVALS、SMEMBERS、
SUNION/SUNIONSTORE、SINTER/SINTERSTORE、SDIFF/SDIFFSTORE、ZRANGE/ZREVRANGE、ZRANGEBYSCORE/ZREVRANGEBYSCORE、
ZREMRANGEBYRANK/ZREMRANGEBYSCORE、DEL、KEYS

具体原因有以下:

  • 在内存操作数据的时间复杂度太高,消耗的CPU资源较多。
  • 一些范围命令一次返回给客户端的数据太多,在数据协议的组装和网络传输的过程就要变长,容易延时。
  • Redis虽然使用了多路复用技术,但是复用的还是同一个线程,这一个线程同一时间只能处理一个IO事件,像一个开关一样,当开关拨到哪个IO事件这个电路上,就处理哪个IO事件,所以它单线程处理客户端请求的,如果前面某个命令耗时比较长,后面的请求就会排队,对于客户端来说,响应延迟也会变长。

解决方案:

分批次,每次获取尽量少的数据,数据的聚合在客户端做,减少服务端的压力。

🍊 大key的存储和删除

当存储一个很大的键值对的时候,由于值非常大,所以Redis分配内存的时候就会很耗时,此外删除这个key也是一样耗时,这种key就是大key。开发者可以通过设置慢日志记录有哪些命令比较耗时,命令如下:

命令执行耗时超过10毫秒,记录慢日志

CONFIG SET slowlog-log-slower-than 10000

只保留最近1000条慢日志

CONFIG SET slowlog-max-len 1000

后面再通过SLOWLOG get [n]查看。

对于大key可以通过以下命令直接以类型展示出来,它只显示元素最多的key,但不代表占用内存最多,命令如下:

#-h:redis主机ip
#-p: redis端口号
#-i:隔几秒扫描
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

对于这种大key的优化,开发者事先在业务实现层就需要避免存储大key,可以在存储的时候将key简化,变成二进制位进行存储,节约redis空间,例如存储上海市静安区,可以对城市和区域进行编码,上海市标记为0,静安区标记为1,组合起来就是01,将01最为key存储起来比上海市静安区作为key存储起来内存占比更小。

可以将大key拆分成多个小key,整个大key通过程序控制多个小key,例如初始阶段,业务方只需查询某乡公务员姓名。然而,后续需求拓展至县、市、省。开发者未预见此增长,将数据存储于单个键中,导致键变成大键,影响系统性能。现可将大键拆分成多个小键,如省、市、县、乡,使得每级行政区域的公务员姓名均对应一个键。

根据Redis版本不同处理方式也不同,4.0以上版本可以用unlink代替del,这样可以把key释放内存的工作交给后台线程去执行。6.0以上版本开启lazy-free后,执行del命令会自动地在后台线程释放内存。

使用List集合时通过控制列表保存元素个数,每个元素长度触发压缩列表(ziplist)编码,压缩列表是有顺序并且连续的内存块组成的一种专门节约内存的存储结构,通过在redis.conf(linux系统)或者redis.windows.conf(windows系统)文件里面修改以下配置实现:

list-max-ziplist-entries 512
list-max-ziplist-value 64

🍊 数据集中过期

在某个时段,大量关键词(key)会在短时间内过期。当这些关键词过期时,访问Redis的速度会变慢,因为过期数据被惰性删除(被动)和定期删除(主动)策略共同管理。惰性删除是在获取关键词时检查其是否过期,一旦过期就删除。

这意味着大量过期关键词在使用之前并未删除,从而持续占用内存。主动删除则是在主线程执行,每隔一段时间删除一批过期关键词。若出现大量需要删除的过期关键词,客户端访问Redis时必须等待删除完成才能继续访问,导致客户端访问速度变慢。

这种延迟在慢日志中无法查看,经验不足的开发者可能无法定位问题,因为慢日志记录的是操作内存数据所需时间,而主动删除过期关键词发生在命令执行之前,慢日志并未记录时间消耗。

因此,当开发者感知某个关键词访问变慢时,实际上并非该关键词导致,而是Redis在删除大量过期关键词所花费的时间。

(1)开发者检查代码,找到导致集中过期key的逻辑,并设置一个自定义的随机过期时间分散它们,从而避免在短时间内集中删除key。

(2)在Redis 4.0及以上版本中,引入了Lazy Free机制,使得删除键的操作可以在后台线程中执行,不会阻塞主线程。

(3)使用Redis的Info命令查看Redis运行的各种指标,重点关注expired_keys指标。这个指标在短时间内激增时,可以设置报警,通过短信、邮件、微信等方式通知运维人员。它的作用是累计删除过期key的数量。当指标突增时,通常表示大量过期key在同一时间被删除。

相关文章
|
2月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
2月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
127 8
|
3月前
|
Java Spring
如何优化Java异步任务的性能?
本文介绍了Java中四种异步任务实现方式:基础Thread、线程池、CompletableFuture及虚拟线程。涵盖多场景代码示例,展示从简单异步到复杂流程编排的演进,适用于不同版本与业务需求,助你掌握高效并发编程实践。(239字)
235 6
|
3月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
3月前
|
消息中间件 人工智能 Java
抖音微信爆款小游戏大全:免费休闲/竞技/益智/PHP+Java全筏开源开发
本文基于2025年最新行业数据,深入解析抖音/微信爆款小游戏的开发逻辑,重点讲解PHP+Java双引擎架构实战,涵盖技术选型、架构设计、性能优化与开源生态,提供完整开源工具链,助力开发者从理论到落地打造高留存、高并发的小游戏产品。
|
3月前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
126 0
|
4月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
136 4
|
4月前
|
安全 Java 数据库
Java 项目实战病人挂号系统网站设计开发步骤及核心功能实现指南
本文介绍了基于Java的病人挂号系统网站的技术方案与应用实例,涵盖SSM与Spring Boot框架选型、数据库设计、功能模块划分及安全机制实现。系统支持患者在线注册、登录、挂号与预约,管理员可进行医院信息与排班管理。通过实际案例展示系统开发流程与核心代码实现,为Java Web医疗项目开发提供参考。
218 2
|
4月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
237 3