# 并发下数据库commit提交时间突然变长

# 背景

项目调用其他项目接口, 保存大批量订单数据, 接口拿到批量订单数据, 直接拆成单个订单, 发到MQ中.

MQ并发消费场景下, 项目中获取流水号的接口提交事务的时间突然变长.

# 故事主线

故事的主人公是获取流水号接口, 在并发消费场景下出现了性能问题, 下面先说下此接口的实现

# 获取流水号接口实现逻辑

接口需求: 调用此接口能获取到对应业务的流水号, 流水号为固定步长递增.

  1. 接口参数: 流水号类型(根据业务不同设定不同枚举)
  2. 接口方法上加如下注解, 保证事务独立.
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
  1. 流水号数据表设计
CREATE TABLE `sys_sequence` (
  `seq_name` varchar(50) NOT NULL COMMENT '名称(对应枚举value)',
  `seq_prefix` varchar(10) DEFAULT NULL COMMENT '前缀',
  `current_value` bigint(20) NOT NULL COMMENT '当前值',
  `increment_val` int(11) DEFAULT '1' COMMENT '步长',
  `need_suffix` char(2) DEFAULT '0' COMMENT '0不需要后缀,1需要一个4位数随机数的后缀',
  `c_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(数据库自己维护)',
  `u_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间(数据库自己维护)',
  PRIMARY KEY (`seq_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 实现细节
    1. 查询出当前业务流水号类型的数据, 此处用for update给数据加锁
    2. 根据当前值步长是否需要后缀计算出新值
    3. 更新数据表, 当前值=当前值+步长

# 场景说明

在50-100并发的情况下, 此接口耗时很久, 久的长达20秒, 快的也要5秒.

根据实现逻辑推断, 大部分并发线程都卡在了此处的for update上, 虽然此接口作为新起的事务提交了, 但却影响了整体的性能.

# 解决方案

将获取流水号接口的压力从mysql改为redis.

当需要流水号时, 先去从redis中获取, 如果redis没有流水号, 则从mysql中批量获取并生成到redis中. 但这种方案在并发时, 并且redis中没有号时去拿号, 会发生较长的等待, 导致性能降低. 有两个方案解决, 可以使用一个daemon线程或者定时线程去维护redis中的号, 当其低于某个特定值时就去生成流水号存入redis中, 或者在每次拿号时去判断如果流水号数量少于某个值, 则异步去mysql中批量获取并生成到redis中, 只是此时需要保证只有一个线程在生成号.

以上方案需要注意线程安全, 并发安全.

修改于: 8/11/2022, 3:17:56 PM