1 中间件实践总结
为什么要性能调优?
1)高并发场景下会遇到各种问题
2)充分利用服务器资源,降低成本
1.1 tomcat调优
Tomcat 的关键指标有吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。前三个指标是我们最关心的业务指标,Tomcat 作为服务器,就是要能够又快有好地处理请求,因此吞吐量要大、响应时间要短,并且错误数要少。后面三个指标是跟系统资源有关的,当某个资源出现瓶颈就会影响前面的业务指标,比如线程池中的线程数量不足会影响吞吐量和响应时间;但是线程数太多会耗费大量 CPU,也会影响吞吐量;当内存不足时会触发频繁地 GC,耗费 CPU,最后也会反映到业务指标上来
主要参数
|
1.2 jvm调优
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式,一般而言JVM调优对于机器性能提升有限,效果远不如优化代码收益高。
|
线上分析工具
线程堆栈:Universal JVM GC analyzer - Java Garbage collection log analysis made easy Smart Java thread dump analyzer - thread dump analysis in seconds arthas
堆内存分析:MAT(Memory Analyzer Tool)
1.3 mysql
可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
1、无索引行锁会升级为表锁,锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
update account set balance = 800 where name = 'lilei';
其他对该表任一行操作都会阻塞住
解决方法:加索引或者锁定某一行用for update(排它锁),select * from test_innodb_lock where name = 'lilei' for update; 这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交
2、间隙锁,在某些情况下可以解决幻读问题。
例如:id: 1 2 3 10 20, 间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,update account set name = 'zs' where id > 8 and id <18; 则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
解决方法:通过id更新
锁优化建议:尽可能减少检索条件范围,避免间隙锁,尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行。
3、分布式事务
1)保留主要数据:当调用只有一个重要接口时,可调整程序顺序,保留主要数据。
2)阻塞式重试:当请求一个微服务的 API 失败后,发起三次重试。如果三次还是失败,就打印日志,继续执行或向上层抛出错误,接口幂等、调用失败做定时任务补偿,适用于业务对一致性要求不敏感的场景, 这种场景最多
3)异步队列:在当前服务将数据写入 DB 后,推送一条消息给 MQ,由独立的服务去消费 MQ 处理业务逻辑
4)TCC 补偿事务:seata,Himly
5)本地消息表:具体做法是在本地事务中插入业务数据时,也插入一条消息数据。然后在做后续操作,如果其他操作成功,则删除该消息;如果失败则不删除,异步监听这个消息,不断重试。
分布式事务的 6 种解决方案,写得非常好!-腾讯云开发者社区-腾讯云
1.4 redis
缓存穿透与缓存击穿
public String getClickId(ClickIdDTO clickIdDTO) {
final Integer channelCode = clickIdDTO.getChannelCode();
final String requestRefer = clickIdDTO.getRequestRefer();
//是否在布隆过滤器存在
if (!redisBloomFilter.exist(PutinBloomFilterEnum.BACK_REPORT_CHANNEL, channelCode)) {
log.info("channel not exist, channel:{}", channelCode);
return null;
}
//查询缓存
PlatformCallbackHandlerEntity platformCallbackHandlerEntity = cacheService.get(PutinCacheEnum.BACK_REPORT_CHANNEL, channelCode.toString(), PlatformCallbackHandlerEntity.class);
if (platformCallbackHandlerEntity != null) {
return getClickIdByRefer(platformCallbackHandlerEntity, requestRefer);
}
return LockSimpleTemplate.executeWithTryLock(RedisLockKeyBackEnum.REPORT_CHANNEL_LOCK,
() -> {
//查询缓存
PlatformCallbackHandlerEntity handlerEntity = cacheService.get(PutinCacheEnum.BACK_REPORT_CHANNEL, channelCode.toString(), PlatformCallbackHandlerEntity.class);
if (handlerEntity != null) {
return getClickIdByRefer(handlerEntity, requestRefer);
}
//查询数据库
PlatformCallbackHandlerEntity strategy = platformCallbackHandlerService.getByReportChannel(channelCode);
if (strategy != null) {
return getClickIdByRefer(strategy, requestRefer);
}
return null;
}, channelCode);
}
@Component
@Slf4j
public class RedisBloomFilter {
@Autowired(required = false)
private RedissonClient redissonClient;
private static final ConcurrentHashMap<String, RBloomFilter<Object>> filterMap = new ConcurrentHashMap<>();
public <T> void add(PutinBloomFilterEnum bloomFilterEnum, T value) {
getRBloomFilter(bloomFilterEnum).add(value);
}
public <T> void add(PutinBloomFilterEnum bloomFilterEnum, T... values) {
RBloomFilter<T> rBloomFilter = getRBloomFilter(bloomFilterEnum);
for (T v : values) {
rBloomFilter.add(v);
}
}
public <T> boolean exist(PutinBloomFilterEnum bloomFilterEnum, T value) {
try {
return getRBloomFilter(bloomFilterEnum).contains(value);
} catch (Exception e) {
log.error("error", e);
}
return true;
}
/**
* 只要有一个包含,就算包含
* @param bloomFilterEnum
* @param values
* @return
*/
public <T> boolean existAnyOne(PutinBloomFilterEnum bloomFilterEnum, T... values) {
try {
RBloomFilter<T> rBloomFilter = getRBloomFilter(bloomFilterEnum);
for (T v : values) {
if (rBloomFilter.contains(v)) {
return true;
}
}
return false;
} catch (Exception e) {
log.error("error", e);
}
return false;
}
/**
* 统计存在数量
*
* @param bloomFilterEnum
* @return
*/
public long countNum(PutinBloomFilterEnum bloomFilterEnum) {
return getRBloomFilter(bloomFilterEnum).count();
}
private <T> RBloomFilter<T> getRBloomFilter(PutinBloomFilterEnum bloomFilterEnum) {
String boomFilterKey = getBoolFilterKey(bloomFilterEnum.getCode());
if(filterMap.containsKey(boomFilterKey)) {
return (RBloomFilter<T>) filterMap.get(boomFilterKey);
}
RBloomFilter<Object> rBloomFilter = redissonClient.getBloomFilter(boomFilterKey);
PutinBloomFilterEnum putinBloomFilterEnum = PutinBloomFilterEnum.getEnum(bloomFilterEnum.getCode());
if(putinBloomFilterEnum != null) {
// 预计统计数量设置比实际要大一些, 降低误判率, 误差率越小,精度越高
rBloomFilter.tryInit(putinBloomFilterEnum.getExpectedInsertions(), putinBloomFilterEnum.getFalseProbability());
}
filterMap.put(boomFilterKey, rBloomFilter);
return (RBloomFilter<T>) rBloomFilter;
}
private String getBoolFilterKey(String key) {
return EwpThreadLocal.getPartnerCode() + ":" + key;
}
}
1.5 xxl-job
数据逻辑分片、物理分片、高性能处理
/**
* 服务参数
*/
@Data
public class TaskServerParam implements Serializable {
private static final long serialVersionUID = -8075352114642218846L;
/**
* 获取数据条数
*/
private int fetchCount = 200;
/**
* 单次执行多少条
*/
private int executeCount = 20;
/**
* 线程数
*/
private int threadCount = 10;
private Integer partnerCode;
/**
* 业务数据
*/
private String bizContent;
}
@Slf4j
@Component
public class CommonTestJob extends AbstractScheduleTaskProcess<OrderJobDTO> {
@EwpMethodJob("common_testJob")
public void commonTestJob() {
try {
execute();
} catch (Exception exception) {
log.error("#commonTestJob error:", exception);
}
}
@Override
protected List<OrderJobDTO> selectTasks(TaskServerParam taskServerParam, int curServer) {
// TODO: 2023-04-02 获取任务数据
// 获取数据条数 taskServerParam.getFetchCount();
// 1)物理分片:
// where run_status = #{runStatus}
// and run_time < 6
// and next_run_time < now()
// and mod(id, #{shardTotal}) = #{shardIndex}
// limit #{limit}
// 2)逻辑分片:
// 数据所有数据:id % shardTotal == shardIndex;
// 3) 高性能场景:
// 任务表存储计算好 shardIndex, 直接获取
return new ArrayList<>();
}
@Override
protected void executeTasks(List<OrderJobDTO> tasks) {
// TODO: 2023-04-02 任务处理
}
}
/**
* 任务抽象类
* @param <T>
*/
@Slf4j
public abstract class AbstractScheduleTaskProcess<T> {
/**
* 任务线程
*/
private ThreadPoolExecutor executor;
private volatile int lastThreadCount = 0;
/**
* 执行任务
* 获取业务条数,然后根据每次执行条数设置线程数量
* @return
*/
protected void execute() {
String jobParam = EwpJobUtils.getJobParam();
if(StringUtils.isEmpty(jobParam)) {
throw new EwpException("job参数信息为空!");
}
TaskServerParam serverParam = JSON.parseObject(jobParam, TaskServerParam.class);
EwpThreadLocal.setPartnerCode(serverParam.getPartnerCode());
PutInTraceIdHelper.initTraceId();
int curServer = EwpJobUtils.getShardIndex();
if (log.isInfoEnabled()) {
log.info("开始执行任务[" + this.getClass().getName() + "][" + curServer + "]....");
}
//获取任务
List<T> tasks = this.selectTasks(serverParam, curServer);
if (log.isInfoEnabled()) {
log.info("获取任务[" + this.getClass().getName() + "]共" + (tasks == null ? 0 : tasks.size()) + "条");
}
//执行任务
if (!CollectionUtils.isEmpty(tasks)) {
this.executeTasksInner(serverParam, tasks);
}
}
private void executeTasksInner(TaskServerParam param, List<T> tasks) {
int threadCount = param.getThreadCount();
synchronized(this) {
if (this.executor == null) {
this.executor = (ThreadPoolExecutor) CoreUtil.createCustomExecutorService(threadCount, "dts.executeTasks");
this.lastThreadCount = threadCount;
} else if (threadCount > this.lastThreadCount) {
this.executor.setMaximumPoolSize(threadCount);
this.executor.setCorePoolSize(threadCount);
this.lastThreadCount = threadCount;
} else if (threadCount < this.lastThreadCount) {
this.executor.setCorePoolSize(threadCount);
this.executor.setMaximumPoolSize(threadCount);
this.lastThreadCount = threadCount;
}
}
//数据分隔
List<List<T>> lists = CoreUtil.splitList(tasks, param.getExecuteCount());
final CountDownLatch latch = new CountDownLatch(lists.size());
Iterator iterator = lists.iterator();
while(iterator.hasNext()) {
final List<T> list = (List)iterator.next();
this.executor.submit(new Runnable() {
@Override
public void run() {
try {
AbstractScheduleTaskProcess.this.executeTasks(list);
} catch (Exception exception) {
throw exception;
} finally {
latch.countDown();
}
}
});
}
try {
latch.await();
} catch (InterruptedException exception) {
throw new RuntimeException("interrupted when processing data access request in concurrency", exception);
}
}
protected abstract List<T> selectTasks(TaskServerParam taskServerParam, int curServer);
protected abstract void executeTasks(List<T> tasks);
}
1.6 dubbo
provider端:
threads:业务线程池, 默认:200, 可是适当调大
iothreads:io线程池大小,CPU+1
queues: 线程池队列大小,当线程池满时,排队等待执行的队列大小,建议不要设置,当线程程池时应立即失败,重试其它服务提供机器,而不是排队,除非有特殊需求
consumer端:
timeout: 超时时间
retries: 重试次数
接口总结:
1、接口幂等性设计,比如:消费方加业务bizId,服务提供方根据bizId去重
2、能缓存的数据要缓存
3、接口入参个数不能超过3个,入参,出参必须实现Serializable接口,入参出参中禁止出现Object对象,必须是其子类,入参出参字段不能使用枚举值,出参不建议使用Map或者JSON这种对象
4、批量查询接口必须要分页,每页不超过100条,能调批量的接口不要调单个的
5、对外接口需要配置限流策略
6、sdk的pom文件,不能引用第三方包,若引用必须使用provided或者optional属性标记
7、接口必须添加注释,清晰描述接口的功能和参数的含义,注释包含字段是否必传
8、当方法入参大于1个且有基本类型(如String,Long)时,封装成对象进行传递
9、一般不要重试,写数类的接口禁止重试
10、在sdk外层目录添加代码升级的changelog
11、对外接口含错误码的,禁止错误码不区别业务,统一返回500系统异常的吞异常行为
12、所有对外提供接口,必须返回包装对象,并且约定错误码,不建议抛出异常
13、对外接口需要添加权限校验
业务需求:李运华5W1H8C1D
异步调用:提高并发能力,如一个接口中调用多个接口 服务降级:比如在电商大促期间,不重要接口降级 本地存根:比如缓存返回结果数据
1.7 mq
1、消息幂等:业务主键+版本号或者处理状态去重、唯一索引
2、消息积压:
浅谈如何解决RocketMQ消息堆积的问题_rocketmq消息堆积解决方案_梦之救赎的博客-CSDN博客
1.8 elasticsearch
当数据写入到ES分片时,会首先写入到内存中,然后通过内存的buffer生成一个segment,并刷到文件系统缓存中(os cache),数据可以被检索(注意不是直接刷到磁盘)ES中默认1秒,refresh一次
默认每隔30分钟会将文件系统缓存的数据刷入到磁盘
1、setting示例
|
写数据:计算路由到指定index,
读数据:根据别名读取
数据删除:指定rollup任务或者手写定时任务删除
2、MySQL 数据同步到 ES 中,大致总结可以分为两种方案
1:监听 MySQL 的 Binlog,分析 Binlog 将数据同步到 ES 集群中,延迟性高
2:直接通过 ES API 将数据写入到 ES 集群中,延迟性低,每次业务操作只更新一次 ES,如果发生错误或者异常,在数据库中插入一条补救任务,有 Worker 任务会实时地扫这些数据,以数据库订单数据为基准来再次更新 ES 数据,实时性要求高的查询走 DB
3、优化
ES 性能调优,这可能是全网最详细的 Elasticsearch 性能调优指南_es调优_Elastic开源社区的博客-CSDN博客