状态机流转
  • 兜底

    • 乐观锁verison来确定本次修改的一致性

    • 乐观锁同时还需要加一个状态判断的条件,是否等于原状态,再去修改为期望的状态

  • 业务层面

    • 定义一个静态变量,指定a状态只能流转为b、c状态,b状态只能流转为c状态

    • 这个结构可以是一个map,key是当前状态,value是集合存储可以变更的状态

    • 变更前进行校验

  • 高并发场景

    • 加一个分布式锁来保证状态流转的顺序性

  • 异步操作下避免乱序

    • 判断业务时间戳是否已经过期

场景

一个类中两个事务互相调用,如何保证事务不失效

spring事务的传播行为

  • 当前存在事务,直接加入当前事务;不存在则新建事务 -- 默认

  • 当前存在事务,挂起当前事务,新事务方式运行

  • 当前存在事务,嵌套子事务方式运行;不存在则新建事务

  • 当前存在事务,加入当前事务;不存在事务抛出异常

  • 当前存在事务,加入当前事务;不存在则以非事务方式运行

  • 当前存在事务,挂起当前事务,以非事务方式运行

  • 当前存在事务,直接以非事务方式运行

默认的传播形式,两个都有事务的函数调用,事务是不会失效的

失效的场景是因为

  • a内部直接调用b,默认就是this.b,这样就会绕过AOP的代理,从而直接用未增强的方法来执行,正常应该注入自身,通过注入调用的方式

  • 本身这个函数就是非public,或者被final修饰,AOP也是不会去增强这个方法,从而导致失效

  • try-catch把异常捕获,框架感知不到异常从而失效

  • 数据库引擎本身不支持事务,例如MyIsam引擎,即使spring下发commit/rollback指令,事务也不会生效

  • 抛出的异常框架不支持,默认支持的就是RuntimeException和Error,如果是IOException那就会捕捉不到

防止超卖

Redis+数据库乐观锁

  • redis来扛住万级的QPS并发

    • 将库存信息预热到缓存中

    • 通过Lua脚本来保证查询和扣减的原子操作

    • 之后在通过MQ方式异步将扣减的信息实际更新到DB中

  • 乐观锁作为一切的兜底

    • update语句增加version版本号的条件,保证更新过程中不会有别的线程已经扣减了库存

    • 更新条件再增加一个库存 > 0的条件,彻底方式超卖

数据库与缓存的双写一致性

  • 旁路缓存

    • 先更新DB,再删除缓存,下次查询时候再写入缓存

    • 更新DB删除缓存时间空窗很短

    • 也不用考虑像延迟双删一样高并发写场景下时序混乱造成的问题

    • 如果需要考虑删除缓存失败,可以加一个消息来处理删除失败的场景进行重试

  • 延迟双删(读多写少业务)

    • 删除缓存数据

    • 更新DB

    • 线程sleep几十ms

    • 再次删除缓存数据

    • 等到下次从DB读取到这个数据后再懒加载更新到缓存中

  • 基于Binlog异步刷新(复杂微服务/夸库同步)

    • 阿里开源canal方式订阅主库的binlog

    • 通过监听binlog更新缓存数据

    • 不需要关心缓存操作,只需要操作DB就行

    • binlog天然有序

  • 分布式锁+事务保证(金融/库存核心业务)

    • 锁可以防止并发

    • 事务可以保证更新结果的一致性

    • 事务提交后删除缓存

    • 可以做一个删除缓存失败的兜底,MQ方式再去重试

缓存击穿、穿透、雪崩

  • 击穿

    • 热点key失效

      • 给热点key设置过期时间尽量长一些

      • value中记录一些比实际过期时间短一些的过期时间

      • 再查询这个key的时候判断下逻辑过期时间是否快到,如果快到那就做一个续期

  • 穿透

    • 请求缓存中不存在数据

      • 布隆过滤器,预热可能存在的数据进去

      • 布隆过滤器返回不存在,则一定不存在;返回存在,有不存在的可能性,但是这个概率可以忽略

      • 对某个时间段内请求次数超过某个阈值的key存储在缓存给定一个比较短的过期时间

      • 核心表可以设置QPS上限,宁愿请求返回失败,也不能把DB打垮

      • 增加互斥锁,在同一时间只能有一个线程来读取

  • 雪崩

    • 大量key同时失效

redis分布式锁如何实现,可重入、看门狗、公平非公平

  • 加锁过程查询和写入Lua脚本保证原子性

  • 可重入是存储结构

    • key是锁值

    • value是hash结构,UUID+线程ID+重入次数

  • 看门狗默认30秒,之后续期10秒

  • 默认是非公平getLock,释放锁后,会唤醒所有等待线程来抢锁

  • 公平getFairLock,内部维护了一个ZSet,标准的FIFO队列

  • 释放锁查询和写入也是Lua保证原子性

redis和zk分布式锁的区别

  • redis是AP -- 高可用,但是锁可能丢失或重复

    • 默认非公平锁

  • ZK是CP -- 牺牲可用性,保证锁不丢失不重复

    • 默认公平锁

大key

  1. 业务层面拆分

    1. 拆分为多个hash结构

    2. 对于大的List、ZSet,分页或分段拆分,根据id、时间范围进行拆分

  2. 引入中间件

    1. ES,redis存储id信息,查到id后再去ES查询,或者可以直接查ES不走redis

    2. MongoDB,redis存储id信息,查到id后再去MongoDB查询

MQ

  • 消费者组

    • 相同消费行为的消费者集合

    • 同一消费者组内的消息,同一个消息默认只会有一个消费者来消费(集群模式)

    • 如果是广播模式那同一个消息在消费者组内所有的消费者都会消费一次。

    • broker以消费者组来记录消费位点,即使宕机也不会丢失消费进度

  • 生产者组

    • 归属同一业务,发送相同类型消息的生产者集合

    • 同一生产者组可以存在多个topic,同一topic也可以由多个不同的生产者组来发送

    • broker作为回查事务状态时的‘寻址标识’,需要确认某条半消息的最终状态,broker就会向生产者组中任意实例发送回查请求,确认到底发送还是回滚

  • 半消息(事务消息)

    • 为了解决‘消息处理’和‘本地事务’的顺序性

      • 先消息,后事务,如果发送了消息并消费成功后,事务回滚了,那两个数据不一致

      • 先事务,后消息,事务成功后,消息因为宕机等问题失败,数据不一致

    • 生产者发送半消息到broker,执行具体的本地事务逻辑,根据本地事务结果向broker提交二次确认

    • 第一次的消息先持久化记录到broker,半消息到topic不会有消费者

    • 第二次提交的确认,broker再来实际转为有消费者的topic来消费

    • 二次确认如果某个原因失败了,broker需要有一个兜底的定时任务来查询半消息的最终状态

  • 顺序消费

    • 发送端

      • 需要保证顺序的消息必须按顺序发送到同一Queue,不能用默认的轮询/哈希策略

      • 需要重写哈希选择器的逻辑,例如用同一业务ID

      • 发送失败自动重试就会被路由到其他Queue,需要关闭重试,或者指定重试的逻辑

    • broker

      • 同一Queue下默认是FIFO队列,所以只要发送端保证顺序,broker存储默认就是顺序

    • 消费端

      • 对于同一个Queue中用MessageListenerOrderly顺序消费接口

  • 消息积压

    • 生产消息,核心业务限流,非核心业务停止写入,停止重试队列的重试

    • 增加消费实例,同一Queue同一时间只能有一个消费实例在消费,所以扩容也要注意不要大于Queue数量,否则无效

    • 根据机器情况调整消费核心线程数和最大线程数

  • 消息丢失

    • 生产

      • 发送到broker后需要收到ACK回执才算结束,否则就要重试发送

    • broker

      • 生产消息发送过来后,接收到刷盘持久化后再返回ACK

    • 消费

      • 消费成功后返回给broker一个ACK,否则就会有重试逻辑

请求毛刺现象(偶尔一次耗时久)

  1. TCP链接建立耗时/网络抖动

    1. TCP重传,某个数据包丢失,触发重传

    2. DNS解析波动,DNS解析刚好过期

  2. JVM的STW

    1. Minor GC和Full GC都会造成所有线程停顿不可用

    2. CPU监控图会显示突然飙升又骤降

  3. 内部调用第三方API请求超时

    1. 没有设置合理的超时时间

    2. 熔断机制

  4. 缓存击穿

    1. 请求的key突然失效,走到DB RT变久

  5. 多个线程同步争抢同一把锁,大量线程Blocked

    1. 锁竞争造成的是请求耗时久是持续性的,但是也存在刚好争抢到锁的可能

  6. 大对象的序列化和反序列化

    1. 其实也会造成持续性的

什么情况下加什么锁

加锁的目的是为了在并发编程和分布式系统中保证数据的一致性、系统的稳定性

  • Synchronized是JVM内置锁

    • 本质是JVM层面实现单体应用内线程同步机制基于Monitor对象来实现

    • 特点:

      • 自动加锁和释放锁,代码块执行完成或有异常都会自动释放锁,不会造成死锁

      • 不可中断,等待锁的线程不可中断

      • 可重入

      • 非公平锁,有一个锁升级的过程,无锁、偏向锁、轻量级锁、重量级锁

    • 缺点也就是不能尝试获取锁、不能中断等待、不能设置超时时间造成的不灵活

  • ReentrantLock是JUC的显式锁

    • 本质是基于AQS框架实现单体应用内线程同步机制

    • 特点:

      • 手动加锁和释放锁,lock()\unLock(),否则会造成死锁

      • 支持公平和非公平,根据构造函数可以指定

      • 可以中断等待

      • 支持尝试获取锁,tryLock()可设置超时时间

    • 场景:需要高级控制场景如超时获取锁、可中断等待

  • Redis分布式锁

    • 本质是利用redis的单线程特性+SETNX原子命令实现跨JVM、跨服务、跨机器的分布式互斥机制

    • 特点:

      • 分布式场景下微服务集群中协调不同实例间的资源访问

幂等校验和分布式锁

两者互补关系,不是替代关系

  • 幂等:结果的正确性

  • 分布式锁:并发执行的顺序

EventBus

本项目基于 Guava AsyncEventBus 实现了进程内异步领域事件机制:在询报价发布等核心业务完成后,通过事务同步钩子在数据库提交后调用posEvent发送事件;事件路由依赖一个全局的Map<String, BaseEventSubscriber>映射表,其中 key 为事件类型字符串(由@SubscribeEvent注解指定),value 为对应的订阅者实例,该映射表在 Spring 容器初始化阶段由各订阅者 Bean 通过InitializingBean.afterPropertiesSet()自动反射读取注解并注册构建完成,分发时根据事件类型精准匹配消费者;消费端继承统一基类并实现消费方法,由配置类中定义的固定大小线程池(核心与最大线程数均为20、队列容量1024)驱动异步执行,同时在线程切换时透传链路追踪上下文并在任务结束后清理租户信息,从而在不阻塞主业务流程的前提下实现跨模块解耦。

策略+责任链

abc三个策略,目的是做一个风控校验。责任链校验有黑名单、失信人员、入驻时长、受行政处罚金额这四个,其中黑名单和失信人员是强卡,有任意一个直接返回异常提示阻断后续流程,入驻时长、受行政处罚金额是异常提示,检测有异常就封装在一个统一的返回值里面,a策略用黑名单+失信人员责任链,b策略用黑名单+失信人员+入驻时长责任链,c策略用黑名单+失信人员+入驻时长+受行政处罚金额责任链,但是在具体封装责任链的地方不能直接这样写死,需要从diamond配置平台拉取不同策略用什么样的责任链,方便后续热更新。

责任链基类

/** * 风控校验节点接口(责任链中的单个Handler) */ public interface RiskCheckHandler { /** * 执行校验 * @param context 风控上下文 * @return 校验结果(包含是否阻断、异常信息等) */ RiskCheckResult check(RiskCheckContext context); /** * 设置下一个节点(由框架自动注入,业务代码不感知) */ void setNext(RiskCheckHandler next); } /** * 校验节点类型枚举(对应Diamond配置中的key) */ public enum RiskCheckNodeType { BLACKLIST("黑名单", true), // isBlock=true,强卡 DISHONEST_PERSON("失信人员", true), // isBlock=true,强卡 SETTLE_DURATION("入驻时长", false), // isBlock=false,弱提示 PENALTY_AMOUNT("行政处罚金额", false); // isBlock=false,弱提示 private final String desc; private final boolean isBlock; // true=阻断型,false=提示型 } /** * 统一风控返回值 */ @Data public class RiskCheckResult { private boolean passed; // 整体是否通过 private boolean blocked; // 是否被强卡阻断 private String blockMessage; // 强卡时的阻断提示 private List<String> warnings; // 弱提示列表(入驻时长、处罚金额等异常信息) // 静态工厂方法,方便构建 public static RiskCheckResult pass() { ... } public static RiskCheckResult block(String msg) { ... } public static RiskCheckResult warn(List<String> warns) { ... } }

策略基类

/** * 风控策略抽象基类 * 【设计意图】:三个策略的差异仅在于"使用哪条责任链",执行流程完全一致 * 因此抽取公共模板方法,子类只需声明自己的策略名 */ public abstract class AbstractRiskCheckStrategy implements RiskCheckStrategy { @Autowired protected RiskCheckChainBuilder chainBuilder; /** * 子类返回自己在Diamond配置中的key */ protected abstract String getStrategyName(); @Override public RiskCheckResult execute(RiskCheckContext context) { // 1. 动态构建当前策略对应的责任链 RiskCheckHandler chainHead = chainBuilder.buildChain(getStrategyName()); // 2. 执行责任链 return chainHead.check(context); } } // 具体策略实现(极其简洁) @Service("STRATEGY_A") public class StrategyA extends AbstractRiskCheckStrategy { @Override protected String getStrategyName() { return "STRATEGY_A"; } } @Service("STRATEGY_B") public class StrategyB extends AbstractRiskCheckStrategy { @Override protected String getStrategyName() { return "STRATEGY_B"; } } @Service("STRATEGY_C") public class StrategyC extends AbstractRiskCheckStrategy { @Override protected String getStrategyName() { return "STRATEGY_C"; } }

动态责任链构建

@Component public class RiskCheckChainBuilder { @Autowired private Map<String, RiskCheckHandler> handlerMap; // Spring自动注入所有Handler Bean @Autowired private RiskCheckConfigDiamond diamondConfig; // Diamond配置监听器 /** * 根据策略名动态构建责任链头节点 * 【核心逻辑】:从Diamond读取该策略的节点列表 → 按序组装链表 */ public RiskCheckHandler buildChain(String strategyName) { // 1. 从Diamond获取当前策略的校验节点类型列表(热更新后自动生效) List<RiskCheckNodeType> nodeTypes = diamondConfig.getNodeTypes(strategyName); if (CollectionUtils.isEmpty(nodeTypes)) { throw new BizException("未配置风控策略: " + strategyName); } // 2. 按配置顺序组装责任链 RiskCheckHandler head = null; RiskCheckHandler current = null; for (RiskCheckNodeType nodeType : nodeTypes) { // 从Spring容器中获取对应Handler实例(注意:每次构建需clone或新建,避免并发问题) RiskCheckHandler handler = getHandlerInstance(nodeType); if (head == null) { head = handler; current = handler; } else { current.setNext(handler); current = handler; } } return head; } /** * 获取Handler实例(避免单例Bean在多线程下next指针混乱) * 【重要】:每个Handler应设计为无状态,或使用原型模式/手动new */ private RiskCheckHandler getHandlerInstance(RiskCheckNodeType nodeType) { // 方案A:Handler是无状态的,直接从map取(推荐,性能高) // 方案B:Handler有状态,用ApplicationContext.getBean()获取原型Bean return handlerMap.get(nodeType.name()); } }

具体执行

@Component("BLACKLIST") @Slf4j public class BlacklistCheckHandler implements RiskCheckHandler { private RiskCheckHandler next; @Override public void setNext(RiskCheckHandler next) { this.next = next; } @Override public RiskCheckResult check(RiskCheckContext context) { // 【强卡逻辑】:命中黑名单直接返回block,不再调用next if (isInBlacklist(context.getUserId())) { log.warn("用户{}命中黑名单,阻断流程", context.getUserId()); return RiskCheckResult.block("用户在黑名单中,禁止操作"); } // 未命中,传递给下一个节点 if (next != null) { return next.check(context); } return RiskCheckResult.pass(); } } @Component("SETTLE_DURATION") @Slf4j public class SettleDurationCheckHandler implements RiskCheckHandler { private RiskCheckHandler next; @Override public void setNext(RiskCheckHandler next) { this.next = next; } @Override public RiskCheckResult check(RiskCheckContext context) { // 【弱提示逻辑】:异常时收集warning,但仍继续执行后续节点 List<String> warnings = new ArrayList<>(); if (getSettleDays(context.getUserId()) < 90) { warnings.add("入驻时长不足90天,请注意风险"); } // 无论是否有warning,都继续传递(除非next为空) RiskCheckResult nextResult = (next != null) ? next.check(context) : RiskCheckResult.pass(); // 合并当前warning到下游结果中 if (!warnings.isEmpty()) { nextResult.getWarnings().addAll(warnings); nextResult.setPassed(false); // 有warning则整体不pass,但不block } return nextResult; } }

DDD

“在我们的项目中,我们用充血模型让订单对象自己封装了状态机流转逻辑,避免了Service层的if-else爆炸;以订单为聚合根,订单项作为内部实体,保证了下单操作的原子性;对接其他上游系统时,通过防腐层将其接口转换为本域的可以理解的领域对象,即使上游系统做了调整,我们只需要优化防腐层就好了,实际使用领域是不用关注它这个改动的”

聚合根

  1. 入口唯一:order.addItem()✅ /orderItem.setQuantity()

  2. 事务边界:一个聚合 = 一个数据库事务单元。跨聚合绝对不能强事务,只能用领域事件实现最终一致性。

  3. ID引用:其他聚合只能通过聚合根的ID来关联,不能持有对象引用(如Order里存User对象❌,存userId✅)。

  • 防止业务规则被绕过导致数据不一致;控制事务粒度,避免大事务锁表;降低对象间的耦合度。

  • 聚合要尽可能小。只把“必须在同一事务内保持一致”的对象放进同一个聚合。能通过事件异步处理的,坚决拆出去。

防腐层

调用外部服务的隔离层,就是本项目与外部接口之间做了一层转换,确保转换后的领域对象是本域可以理解,且外部的调整变化不会直接影响到本域

贫血模型/充血模型

  • 充血模型:包含属于该对象的业务行为和方法。状态变更必须通过对象自身的方法完成,禁止外部直接修改属性。

    • order.pay()订单自己知道如何支付、校验状态、计算金额,Service只负责编排流程。

跨模型调用

例如询报价发布接口中,会调用需求表来更新需求关联的询报价状态,所以如果直接在询报价Facade中注入需求service,那就破坏了DDD领域聚合的设计思路,所以首先要看业务上是强一致还是最终一致

  • 强一致:

    • 同步事件发布的方式

    • 上游编排服务。在询报价、需求模型上面再增加一层来编排这个多模型间调用的服务,各自领域还是做自己的事。

  • 最终一致

    • 异步事件发布

    • MQ消息订阅

MQ

目的

  • 解耦

  • 异步

  • 流量削峰

缺点

  • 可用性降低 (MQ集群解决)

  • 复杂性提升

  • 一致性问题

运行架构

RocketMQ

producer

生产

consumer

消费

broker

处理消息

持久化消息

nameServer

收集和记录broker的信息 -- 端口、ip、可用状态

producer、consumer都是从nameServer上拉取broker的信息

消息模型

RocketMQ

topic

逻辑概念,不存储具体消息

messageQueue

存储具体消息

消息不丢

rocketMQ

producer端:

  • 单向发送:无返回值,只管发,容易造成消息丢失

  • 同步发送:sendResult返回值(有是否发送成功的状态、发送到哪个queue、queue的编号),消息可靠,阻塞,影响性能

  • 异步发送:无返回值,提供异步回调接口(发送成功函数、发送异常函数),服务端压力大,需要等回调

consumer端:

集群模式

  • 一个消息只会被一个消费者组中的多个消费者实例共同处理一次

广播模式

  • 一个消息会推送给所有消费者处理,不关心消费者组

数据库

索引存储结构

  • 二叉树

    • 不支持自动旋转,可能退化为链表,层级过高,IO复杂度升高

  • 二叉平衡树(红黑树)

    • 支持自平衡,随着数据量增大层级增高,IO复杂度升高,每次为了平衡也会消耗资源

  • Hash表

    • 数组+链表结构,计算hash值进行存储,不支持范围查询,存在hash冲突(非核心问题)

  • B树

    • 普遍3个层级就可以存储近亿条数据,4个层级几十亿,不存在数据量这么大的情况,所以IO复杂度低

    • 每个节点默认存储16KB大小的索引

    • 每个层级的索引下都会挂载行相关数据

    • 因为数据挂载在节点的索引上,所以一个磁盘块16KB存储的数据量有限,造成层级高,IO复杂度提升

  • B+树

    • 结构与B树相同

    • 非叶子节点不存储行数据,存储了索引值和指针(指向下一节点)

    • 叶子节点存储索引+行数据,且叶子节点间用双向指针连接

存储引擎

  • MyIsam存储引擎

    • 不支持事务

    • 不支持行锁

    • 索引与表数据单独存储,B+树的叶子节点存储的是索引值和表数据在磁盘的地址值。所以即使主键索引它也不存在聚簇索引

  • InnonDB存储引擎

    • 索引与表数据存储在一块,B+树叶子节点存储的是索引值和对应这一行的完整数据

    • 主键索引叶子结点存储的这一行完整数据,也就是聚簇索引

    • 非主键索引的叶子节点存储的是索引值+主键值,具体查询时会再回表查一次

    • 如果没有主键索引,那会选择唯一索引来维护一个叶子节点存储所以数据的B+树

    • 既没有主键,又没有唯一,那就会用隐藏列RowId来维护一个叶子节点存储所有数据的B+树

    • 主键索引--整型+自增,因为索引存储是从左往右连续自增存储,如果非自增索引新增进来可能会造成整个索引的调整消耗资源

      • 分库分表,主键索引就建议用雪花算法,虽不是自增,但是趋势递增,所以就可用

索引类型

  • 主键索引

    • 聚簇索引、覆盖索引

  • 唯一索引

    • 限制当前列数据唯一,可以存多个null

  • 联合索引

  • 普通索引

事务

ACID

  • A:原子性:undo log来保证同时成功或同时失败

  • C:一致性:事务的最终目的,其它三个特性和业务代码共同来支持

  • I:隔离性:事务并发操作,互不干扰。数据库的四个隔离级别。锁和MVCC来保证

  • D:持久性:redo log来保证

长事务优化

  • 查询放在事务外

  • 事务内避免远程调用,要设置超时时间

  • 一个事务内避免操作太多数据,可以拆分为多个小事务

  • 更新等涉及加锁的操作放在事务靠后的位置

  • 能异步处理最好就用异步

  • 业务代码保证数据一致性,按照非事务方式执行

事务隔离级别

  • 读未提交

    • 脏读

  • 读已提交 -- 使用场景:并发性能要求高

    • 不可重复读:a事务内在多次读,b、c、d线程在多次改。a每次读到的数据都不同

  • 可重复读 -- 使用场景:数据一致性要求高

    • 在读的a事务内,即使别的线程如何改,a事务在未提交前读到的数据都是第一次的值

    • undo log支持的快照读,read view来读对应事务内的版本数据

    • 幻读:范围查询,第一次读到1条数据,第二次读到3条数据,在两次读到过程中有新增进来

    • 脏写:并发更新时覆盖已经修改过的数据 -- 可以乐观锁版本号方式解决

  • 串行 -- 所有读操作加读锁来支持

    • 任何读写的操作都是顺序执行

    • 性能问题

  • 读锁(共享锁 IS)select...lock in share model;

    • 阻塞所有非读操作

  • 写锁(排它锁 IX)select...for update

    • 阻塞所有读写操作

  • 乐观锁

    • 表字段增加一个version,用来记录所有非读操作给version+1

    • 另一个事务读写的时候带上当前事务的version查询条件

  • 悲观锁

  • 意向锁

    • 加了共享锁、排它锁就不能给当前表加表锁,让表锁和行锁之间能互相感知、避免冲突。

  • 全局锁

  • 表锁

  • 行锁

  • 间隙锁 -- RR解决幻读

    • 最常发生在范围查询情况,且加了悲观锁,很有可能触发间隙锁(左开右开)

  • 临键锁 -- RR解决幻读

    • 行锁 + 间隙锁 组合

    • 可重复读下可防止幻读问题发生,就是给某一行加行锁时,还会锁定当前行到前一行的间隔(左开右闭)

日志

  • undo log

    • MVCC -- read view快照读

  • redo log

    • 两阶段提交,对操作日志是磁盘顺序写

  • bin log

    • 主从同步,宕机恢复

Buffer Pool缓存池

为了让磁盘随机读变为内存读提高效率

写操作先修改Buffer Pool内的数据,随后再有别的线程来刷盘

  • 更新操作,会先更新buffer pool的数据

  • 记录redo log,状态未prepare

  • 准备提交事务,记录bin log,并修改redo log状态为commit

  • IO线程随机时间将buffer pool的数据以page为单位刷入磁盘

LRU

管理缓存页

哪些page该被淘汰,哪些可以保留

  • mysql缓存池内存淘汰策略

  • 最新访问到最旧访问形成一个先后链表,内存不足时淘汰末尾page

慢sql优化

步骤

  • 开启慢sql监控

  • mysqldumpslow查看具体慢sql

  • explain命令查看执行计划

    • type

    • key

    • key_len

    • rows

    • extra

问题

  • 未走索引

    • 索引失效

      • 隐式转换

      • 函数操作

      • 最左前缀

      • or like等关键字影响

    • 字段无索引

  • 查询时检索的数据量大

    • rows字段几万几十万

  • 深分页

  • 临时表

    • 多表join并不会一定生成临时表,分组、去重、合并、复杂排序会生成临时表

    • 取决于是否需要先把查询的数据一部分存起来才能进行下一步

主从复制

流程

  • 从库向主库发送dump请求

  • 主库给从库返回一个bin log dump线程

  • 从库用这个binlog dump线程主动拉取主库binlog日志写入到自己的relay log

  • 从库另一个线程读取realylog并执行这些sql

主从同步延迟

账号注册完后立即登录提示账号不存在,可能就是同步延迟

  • 写操作完成后,读操作指定读主库

  • 第一次未读到数据后,再读一次主库,二次读取,主库压力太大

  • 核心业务读写都用主库,非核心业务读写分离,即使第一次未读到也不会有太大影响

分库分表

水平分表

  • 范围路由:按照id插入,1-100000表1,100001-200000表2

    • 扩容顺滑,原有数据不用动

    • 表数据存储不均匀,不同表请求压力不同

  • hash路由:某一个or某几个列计算hash值存储到不同表

    • 初始表选择几张表,多了太

    • 扩容麻烦,增加表后原有数据需要迁移

    • 数据分布均匀,不同表访问请求均匀

  • 配置路由:建一个路由表,订单id+某表

    • 扩容容易,原数据不动

    • 需要多查询一次

    • 路由表数据量会很大,性能影响

分库不停机数据迁移

  • 阶段一

    • 在线双写,原库继续写入,新库用中间件监听原库来新增数据进行分表存储

    • 通过定时任务等方式把原库老数据同步到新库

  • 阶段二

    • 指定读操作到新库

    • 写保持双写

  • 阶段三

    • 原库停止写操作

分库分表影响

  • 单表事务

    • 分布式事务

  • 跨表join、orderBy、groupBy查询

    • 字段冗余

    • 单表查询在内存聚合

    • 需要查询的表的字段所有都同步到ES,通过ES再来查询返回

  • 自增主键id重复

    • 可以继续用自增id,但是不同表自增的步长需要指定不同

    • 雪花算法

MySQL-JSON 类型

  • 订正sql-不改变原本字段,进行添加

UPDATE `global_linehaul_meta_info` SET `feature` = JSON_SET( `feature`, '$.cpQuotationRuleDTOs', JSON_ARRAY_APPEND( COALESCE(JSON_EXTRACT(feature, '$.cpQuotationRuleDTOs'), '[]'), '$', JSON_OBJECT( 'eqTypes', JSON_ARRAY('SINGLE_BOARD_PIVOT', 'PERIODIC_PIVOT') ) ) ) WHERE `type` = 'LhSupplier' and `env` = 'pre' AND is_deleted = '0';
//执行前 { "adHocSigned" : "UNSIGNED", "supplierCooperateModelDTOs" : [ { "toPortCode" : "SCQ", "fromPortCode" : "BBA", "cooperateMode" : "DSM", "chainLeaderCode" : "LE03094020", "chainLeaderName" : "华茂", "quotationItemCode" : [ "LINEHAUL_STATION_THC" ], "quotationItemName" : [ "货站处理费(THC)" ] } ] } //执行后 { "adHocSigned" : "UNSIGNED", "cpQuotationRuleDTOs" : [ { "eqTypes" : [ "SINGLE_BOARD_PIVOT", "PERIODIC_PIVOT" ] } ], "supplierCooperateModelDTOs" : [ { "toPortCode" : "SCQ", "fromPortCode" : "BBA", "cooperateMode" : "DSM", "chainLeaderCode" : "LE03094020", "chainLeaderName" : "华茂", "quotationItemCode" : [ "LINEHAUL_STATION_THC" ], "quotationItemName" : [ "货站处理费(THC)" ] } ] }