最近有个奇葩性能指标 单个新增接口 要求1000并发下 单个请求响应低于2s
使用jmeter测试了一个最简单的新增接口 发现在1000并发下 排除日志打印 权限校验等一系列操作后 响应时间仍然有6000+ms 后面尝试使用event事件 刚开始时 响应时间在 900ms左右 但是随之事件越推越多 系统性能主键到达瓶颈 最终响应时间仍然不达标
最终决定应付指标 使用BlockingQueue先将对象进行存储 等请求时间结束统一进行批量新增操作 最后响应时间成功达到900ms 完成指标
后面又由于项目使用的mybatisPlus 所以对代码进行拓展 以结合mybatisPlus进行使用 代码如下:
@Component
public class BatchInsertServiceImplFactory {
private Map<String, AbsBaseService> absBaseServiceHashMap=new HashMap<>();
@Autowired
private ApplicationContext applicationContext;
public AbsBaseService getAbs(String serviceBeanName){
return absBaseServiceHashMap.get(serviceBeanName);
}
@PostConstruct
public void register(){
String[] beanNamesForType = applicationContext.getBeanNamesForType(AbsBaseService.class);
for (String s : beanNamesForType) {
AbsBaseService bean = (AbsBaseService)applicationContext.getBean(s);
absBaseServiceHashMap.put(bean.getBeanName(),bean);
}
}
}
这是一个工厂类 用于在类加载阶段 动态的获取所有AbsBaseService接口的实现类 在原文中 作者使用getBeanOfType 但是我本地在尝试时 这个方法获取不到bean 不知道为什么
public interface AbsBaseService<T> extends BaseService<T> {
String getBeanName();
}
这是AbsBaseService接口 定义了一个获取类名方法 同样在AbsBaseEntity定义一个相同的方法 这样可以实现通过AbsBaseEntity与AbsBaseService之间形成映射
public abstract class AbsBaseEntity extends BaseEntity {
public abstract String getBeanName();
}
这是AbsBaseEntity 用于获取AbsBaseService
@Component
public class BatchInsertService<T extends AbsBaseEntity> {
private final BlockingQueue<T> queue=new LinkedBlockingQueue<>();
private static final long TIMEOUT_MS=2000;
private volatile long lastInsertTime=System.currentTimeMillis();
@Autowired
private BatchInsertServiceImplFactory factory;
public BatchInsertService(){
new Thread(()->{
while (true){
try {
List<T> batch=new ArrayList<>();
while (true){
T poll = queue.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
if (null!=poll){
batch.add(poll);
lastInsertTime=System.currentTimeMillis();
}
if (batch.isEmpty()) break;
long idLeTime=System.currentTimeMillis()-lastInsertTime;
if (idLeTime>TIMEOUT_MS) break;
}
if (!batch.isEmpty()){
batchInsert(batch);
}
}catch (InterruptedException e){
Thread.currentThread().interrupt();
}
}
}).start();
}
public boolean save(T t){
return queue.add(t);
}
private void batchInsert(List<T> ts){
int batchSize=10000;
int totalSize=ts.size();
int batchCount=(int) Math.ceil((double)totalSize/batchSize);
if (CollectionUtils.isEmpty(ts)){
return;
}
AbsBaseService<T> abs = factory.getAbs(ts.get(0).getBeanName());
for (int i = 0; i < batchCount; i++) {
int start=i*batchSize;
int end=Math.min((i+1)*batchSize,totalSize);
List<T> ts1 = ts.subList(start, end);
abs.saveBatch(ts1);
}
queue.clear();
}
}
这是实现批量插入的类 也是核心逻辑
通过定义一个线程安全的队列 将所需要新增的对象暂时存储在内存中 并且定义一个超时时间 如果超过这个时间没有新的对象进入 则进行批量新增操作
同时暴露一个新增接口 在相关代码逻辑中进行调用
在执行批量新增操作时 同时进行分批插入 以防止过多数据同时插入产生错误
在具体使用时 先把需要执行批量插入的实体类基础AbsBaseEntity 然后将其对应的serviceImpl实现AbsBaseService 实现这两个方法的返回值需要相同
然后在具体的需要进行批量新增的类中 声明BatchInsertService 并指定泛型 注意 如果是在对应的service中使用 会出现循环依赖问题 这时候可以使用@Lazy解决 或者改变代码中的AbsBaseService为AbsBaseMapper
值得注意的是 这只是一个应付指标的方案 考虑到具体的场景 仍然有很多可以优化的地方 比如 如果请求不断的进入 queue可能越来越大 最终导致OOM 再比如 如果我只想简单的新增一条数据 仍然需要等待两秒的时间 再比如我在最后执行了queue.clear() 可能导致未被新增的数据丢失 还有其他缺点欢迎在评论区指出
工厂方法的部分代码逻辑非本人原创 但是找不到原文了