中级核心技术1--MySQL/Java 并发

MySQL/Java 并发 / Spring / 分布式中间件 / 工程封装 / JWT / 短信风控 / 高并发活动 / 线上故障

MySQL 底层与性能调优

聚簇索引(InnoDB)& 非聚簇索引(MyISAM)核心区别

数据存储形式

InnoDB 聚簇索引:主键索引叶子节点直接存储完整行数据,整张表数据和主键索引绑定在一起; MyISAM 非聚簇索引:索引文件、数据文件完全分离,索引叶子只存数据行物理地址

回表逻辑差异

InnoDB 二级索引叶子存储主键值,查到主键后再走主键索引拿完整数据(回表);主键查询无需回表,速度极快。

MyISAM 所有索引都存储行号,无论主键 / 普通索引,查询数据都要根据行号去数据文件读取,一定会二次寻址。

事务与锁支持

InnoDB 支持事务、MVCC、行级锁、崩溃安全恢复;

MyISAM 不支持事务,只有表锁,宕机容易丢数据。

主键要求

InnoDB 必须有主键,无主键会自动生成隐藏主键;

MyISAM 可无主键

短信日志表选用 InnoDB 的完整原因

  • 短信日志新增频繁,每条下发记录都要插入,高并发插入场景,InnoDB 行锁不会锁住整张表,并发写入性能远优于 MyISAM 表锁;
  • 支持事务:批量同步短信日志、补偿重推日志时,可保证批量插入原子性,避免半截写入产生脏数据;
  • 崩溃恢复:服务器宕机后,InnoDB 依靠 redo/undo 日志自动恢复数据,不会丢失短信记录,MyISAM 极易损坏日志文件;
  • MVCC 多版本读:大量用户同时查询日志时,读写不阻塞,查询不受插入操作阻塞,满足高并发查询需求;

单纯查询快只是次要优势,事务、并发写入、数据安全才是选型核心。

MVCC多版本读

MVCC 完整定义

MVCC 多版本并发控制,是 InnoDB 实现读写不阻塞的核心机制。查询操作不加锁,通过保存数据历史版本,读操作读取快照版本,写操作操作当前最新版本,解决读写互相阻塞问题,支撑四种事务隔离级别。

实现依赖拆分

Undo 日志(核心版本来源)

修改数据前,会把修改前旧数据存入 undo 日志,用来生成数据历史快照,是 MVCC 多版本的根本来源;事务回滚也依靠 undo。

Read View 读视图

用来判断当前事务能看见哪些版本数据,区分未提交、已提交事务数据,控制隔离级别可见性。

Redo 日志和 MVCC 无关

redo 日志作用是崩溃恢复,保证事务提交后数据持久化,只保障宕机不丢数据,不参与多版本快照生成,不属于 MVCC 依赖组件

MySQL 四种事务隔离级别

读未提交(Read Uncommitted)

可以读到其他事务未提交的数据。

问题:脏读、不可重复读、幻读全部存在,生产环境几乎不用。

读已提交(Read Committed,Oracle 默认)

只能读取其他事务已经提交的数据,解决脏读

遗留问题:不可重复读、幻读。

可重复读(Repeatable Read,InnoDB MySQL 默认)

同一事务内多次读取同一数据,结果保持一致,解决脏读、不可重复读

InnoDB 通过 MVCC 快照机制规避普通幻读,但加锁当前读仍会出现幻读。

串行化(Serializable)

最高隔离级别,所有读写都会加共享 / 排他锁,事务串行执行。 彻底解决脏读、不可重复读、幻读;缺点并发性能极差,仅金融核心场景使用。

间隙锁

间隙锁是什么

InnoDB 在 可重复读 隔离级别下,执行范围查询、条件匹配不到数据时,除了匹配到的行加行锁,还会锁住索引记录之间的空隙,这片空隙的锁就叫间隙锁。

作用:阻止其他事务在间隙内插入新数据,从机制上规避普通幻读。

短信记录表并发更新带来的问题

短信表常按手机号、创建时间做范围更新,间隙锁会锁定一段索引区间; 多个事务同时操作相邻区间,互相持有对方需要的间隙锁,形成循环等待,极易触发 MySQL 死锁,业务接口出现大量超时。

规避方案

  1. 更新条件使用唯一精准主键等值匹配,不走范围查询,不会产生间隙锁;
  2. 统一所有事务更新记录的主键顺序,打破死锁循环等待条件;
  3. 缩短事务执行时长,事务内只保留更新 SQL,剔除无关查询、远程调用;
  4. 并发极高场景可适当调整隔离级别为读已提交(RC),RC 模式下无间隙锁。

间隙锁完整细节

短信日志主键索引现有数据:id = 1、3、5、7、9

索引完整分段(记录 + 间隙):

负无穷~1 | 1 ~ 3 | 3 ~ 5 | 5 ~ 7 | 7 ~ 9 | 9 ~ 正无穷

场景 1:范围更新 where id < 5

SQL:update sms_log set status=1 where id < 5;

逻辑说明:

  1. 匹配到的行 id=1、3 会上行锁;
  2. 数据前后、中间空白索引间隙全部上锁;
  3. 其他事务无法在 id<5 区间插入新数据,防止幻读。
场景 2:等值查询无匹配数据 where id=2

数据库不存在 id=2 的数据

逻辑说明:查询条件命中空白间隙,只添加间隙锁,阻止插入 id=2 的数据。

场景 3:间隙锁互相抢占 → 死锁完整流程

事务 A 执行:update sms_log set status=1 where id between 1 and 5;

锁定资源:行 1、3、5 + 间隙 (1,3)、间隙 (3,5)

事务 B 执行:update sms_log set status=1 where id between 3 and 9;

锁定资源:行 3、5、7 + 间隙 (3,5)、间隙 (5,7)

间隙锁整体原理框图

explain 执行计划

explain 核心关键字段解读

  1. id:SQL 执行顺序,id 越大越先执行;相同 id 从上至下执行。
  2. select_type:区分普通查询、联合查询、子查询、派生表。
  3. table:当前访问哪张表 / 临时表。
  4. type(核心) :访问性能等级,从优到劣:system > const > eq_ref > ref > range > index > ALL;出现range注意间隙锁风险,出现ALL代表全表扫描,必须优化索引。
  5. key:实际使用到的索引;null 表示未走索引。
  6. key_len:索引使用长度,可判断联合索引是否满足最左匹配。
  7. rows:预估扫描行数,数值越大性能越差。
  8. Extra :额外关键信息:
    • Using filesort:文件排序,需要建排序索引;
    • Using temporary:创建临时表,多是 group by 无索引;
    • Using index:覆盖索引,无需回表,最优。

千万级短信日志分页完整优化方案

问题根源

传统 limit offset,size:offset 数值越大,MySQL 需要先扫描并丢弃前面 offset 条数据,数据量越大越慢。

分层优化落地步骤
  1. 分页改造:主键游标分页 记录上一页最大主键 id,下一页查询 where id > #{lastId} limit size;仅扫描目标区间数据,无无效扫描,速度大幅提升。
  2. 索引优化 按业务查询条件建立联合覆盖索引,把查询、筛选、排序字段放入索引,避免回表,消除 Using filesort。
  3. 分表拆分 短信日志按日期水平分表,单表控制在百万级以内,避免单表数据膨胀拖慢查询。
  4. 冷热数据分离 近 7 天热数据存 MySQL,历史归档日志同步到 Elasticsearch;ES 支持模糊检索、全文检索、复杂聚合,减轻 MySQL 查询压力。
  5. 兜底缓存 高频查询的日志分页结果存入 Redis,减少重复访问数据库。

短信日志按天分表

按天分表设计方案

分表命名规则

主表sms_log,分表后缀按日期 sms_log_20260801sms_log_20260802,每日凌晨自动创建当天分表,所有分表字段、索引结构完全一致。

分表路由逻辑

写入短信记录时,根据下发时间create_time截取日期,路由到对应日期分表插入;不会写入主表,主表仅作为模板不存业务数据。

索引统一规范

每张分表都创建相同复合索引(手机号、渠道、创建时间),满足筛选、排序需求。

分表后查询处理

(1)分页查询
  • 指定日期范围很小(单天 / 3 天内):直接路由对应 1~3 张分表,游标分页id>lastId查询,避免 limit 大偏移;
  • 跨多天查询:代码层循环查询每张分表,内存合并分页,禁止数据库 union 大量分表,性能极差。
(2)多条件模糊 / 复杂检索

单纯 union 多表效率低,采用冷热分离架构:

  • 热数据(近 7 天):MySQL 分表,精准条件分页、统计直接查库,实时性高;
  • 冷数据(7 天前历史日志):通过 Canal / 定时任务同步至 ES;
  • 前端传时间区间做判断:区间全部在近 7 天走 MySQL;包含久远历史则 ES 检索;跨冷热区间分别查询再合并结果。

Java 集合 & 并发

HashMap1.7、1.8比较

HashMap1.7 并发问题

JDK1.7 底层数组 + 单向链表,头插法扩容,并发 put 会出现环形链表,遍历时死循环、CPU100%;无任何锁,并发场景线程不安全。

JDK1.8 HashMap 优化

  1. 链表插入改为尾插法,扩容不会产生环形链表;
  2. 链表长度阈值:链表节点>8 转红黑树;节点<6 退化为链表;
  3. 引入数组扩容时高低位拆分,优化扩容迁移效率;
  4. 底层依旧无锁,HashMap 本身还是线程不安全,仅规避死循环,不能多线程并发写。

ConcurrentHashMap 线程安全实现

JDK1.7

分段锁 Segment,把数组分成多段,每段独立加 ReentrantLock,并发只锁当前分段。

JDK1.8
  1. 取消分段锁,采用数组节点头 synchronized 锁 + CAS
  2. 新增元素用 CAS 自旋;发生哈希冲突时,锁住当前数组链表 / 红黑树首节点;
  3. 粒度缩小,并发性能远高于 1.7 分段锁;
  4. 扩容、计数都配套 CAS 操作保证并发安全。

IO 密集型批量短信推送线程池参数配置 & 四种拒绝策略

线程池七大基础参数简述

线程池完整七大参数:核心线程数 corePoolSize、最大线程数 maximumPoolSize、阻塞队列 workQueue、空闲线程存活时间 keepAliveTime、时间单位、线程工厂、拒绝策略。

IO 密集型(批量短信推送)参数配置方案

  1. 业务特点:短信推送大量耗时阻塞在网络 IO、第三方接口调用,CPU 多数时间处于空闲状态,可多开线程提升并发。
  2. 核心线程计算公式:corePoolSize = CPU 核心数 × 2,可根据业务并发量适度上调。
  3. 配套参数建议:maximumPoolSize 与核心线程数持平或小幅增加;阻塞队列设置中等长度缓冲,避免瞬时流量直接触发拒绝策略。
  4. 对比区分:CPU 密集型任务(大量数据计算)核心线程数约等于 CPU 核心数,多开线程会造成 CPU 上下文切换损耗。

四种线程池拒绝策略原理与适用场景

  1. AbortPolicy(系统默认) 直接抛出 RejectedExecutionException 异常;适合核心业务,任务不能丢失,代码捕获异常做重试、降级处理。
  2. DiscardPolicy 静默丢弃新来的任务,无日志无异常;适合非核心统计、日志类任务,少量丢失不影响业务。
  3. DiscardOldestPolicy 丢弃队列中等待最久的旧任务,放入当前新任务;适合实时消息推送,最新数据优先级更高。
  4. CallerRunsPolicy 由提交任务的主线程执行当前任务;短信推送业务首选,流量暴涨时天然限流,不会丢失下发任务。

短信推送业务落地推荐组合

核心线程设为 CPU 核心数 2 倍,拒绝策略选用 CallerRunsPolicy,流量突增时主线程同步执行发送逻辑,自动削峰限流,避免短信任务丢失。

volatile 特性 & synchronized 锁升级流程

volatile 核心能力与局限性

  • 两大可见性、有序性保障
    • 保证可见性:一个线程修改 volatile 变量,其他线程能立刻读到最新值,不存在 CPU 缓存脏数据;
    • 禁止指令重排序:读写 volatile 变量前后代码不会被 CPU 乱序执行,可用于双重检查锁单例防止重排。
  • 无法保证原子性

自增 num++、复合赋值 num = num + 1 这类操作是多步指令(读取 - 计算 - 写回),volatile 只能保证单次读写可见,多线程并发自增会出现数据覆盖,不能替代锁保证原子操作。

  • 适用场景:状态标记位、双重校验锁单例,不适合计数、累加场景。

synchronized 底层锁升级完整流程(无竞争→激烈竞争)

锁会从轻量级逐步膨胀,不可逆升级:偏向锁 → 轻量级锁 → 重量级锁

  1. 偏向锁(单线程无竞争) 第一次线程获取锁,将线程 ID 记录到对象头 Mark Word;后续同一线程再次获取无需 CAS,性能最优。
  2. 轻量级锁(多线程交替执行,无并发争抢) 出现其他线程竞争偏向锁,升级轻量级锁;线程通过 CAS 自旋尝试获取锁,不阻塞,自旋消耗 CPU。
  3. 重量级锁(自旋多次失败,并发激烈争抢) 自旋达到阈值仍拿不到锁,升级为操作系统互斥锁;未获取锁的线程进入内核阻塞队列,让出 CPU,开销最大。

ArrayList 与 LinkedList 场景选型对比

底层存储结构差异

  1. ArrayList:底层动态数组,内存连续,支持随机下标访问。
  2. LinkedList:底层双向链表,节点分散存储,仅保存前后节点引用,无随机下标快速访问能力。

查询操作性能

  1. ArrayList:根据下标 get (index) 直接寻址,时间复杂度 O (1),查询速度极快。
  2. LinkedList:调用 get (index) 需要从头 / 尾遍历链表找到对应节点,时间复杂度 O (n),数据量大时查询很慢。

插入、删除操作性能

ArrayList:

  • 尾部追加:无扩容时速度快;
  • 头部 / 中间插入删除:需要移动后方所有元素,数据量大时开销很高;
  • 扩容会创建新数组、拷贝全部元素,存在额外损耗。

LinkedList:

  • 头部、尾部插入删除仅修改节点引用,O (1);
  • 中间位置插入删除:仍需先遍历找到对应下标节点,遍历耗时;
  • 不存在数组扩容、元素拷贝开销。

业务选型总结

  1. 优先选 ArrayList:查询遍历多、尾部新增多,极少中间 / 头部插入删除(日志列表、数据缓存等场景)。
  2. 优先选 LinkedList:高频头部、尾部频繁增删,查询下标访问少的场景。
  3. 补充误区:LinkedList中间插入并不快,耗时主要消耗在遍历定位节点。

后续更新Spring / 分布式中间件 / 工程封装 / JWT / 短信风控 / 高并发活动 / 线上故障

相关推荐
he___H1 小时前
Spring-Configur注解
java·spring
戴西软件1 小时前
戴西CAxWorks.VPG车辆工程仿真软件技术解析(上)——安全仿真体系的自动化构建
运维·网络·数据库·人工智能·算法·安全·自动化
SunnyDays10112 小时前
Java 如何为 PDF 添加、修改和删除书签
java·pdf
灯澜忆梦2 小时前
【MySQL10】进阶篇 | 索引_#1
数据库·sql·mysql
敲上瘾2 小时前
redis常用数据类型与操作方法
数据结构·数据库·redis·缓存
铃木之影2 小时前
Java 版本 RAG 示例(Spring AI + Milvus)
java·人工智能·spring
C++、Java和Python的菜鸟2 小时前
第14章 项目部署(Linux)
java
OceanWaves19933 小时前
mysql 5.7.29 主从同步配置
数据库·mysql·adb
molaoye3 小时前
win10下切换MySQL版本
数据库·mysql