当前位置: 首页 > news >正文

后端接口性能优化分析-程序结构优化

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

  • 常见思路
    • 8.锁粒度避免过粗
      • synchronized
      • redis分布式锁
        • 非原子操作
        • 忘了释放锁
        • 释放了别人的锁
        • 自旋锁
        • 锁重入问题
          • 加锁
          • 解锁
        • 锁竞争问题
          • 读写锁
          • 锁分段
        • 锁超时问题
        • 主从复制的问题
      • 数据库分布式锁
        • 基于数据库表的增删
        • 基于数据库排他锁
    • 9.切换存储方式:文件中转暂存数据
    • 10.优化程序结构
      • 逻辑结构
      • 日志
    • 11.压缩传输内容
    • 12.线程池设计
      • 线程池默认使用无界队列,任务过多导致OOM
      • 线程池创建线程过多,导致OOM
      • 共享线程池,次要逻辑拖垮主要逻辑
      • 线程池拒绝策略的坑,使用不当导致阻塞
      • Spring内部线程池的坑
      • 使用线程池时,没有自定义命名
      • 线程池参数设置不合理
      • 线程池异常处理的坑
      • 线程池使用完毕后,忘记关闭
      • ThreadLocal与线程池搭配,线程复用,导致信息错乱
    • 13.机器问题 (GC、线程打满、太多IO资源没关闭等等)
      • GC
      • 线程
      • 资源
      • 提升服务器硬件
      • 关于JVM调优部分的内容,将会在后续专门的出一些文章,因为目前笔者对这方面理解还不够,所以暂不多做赘述!!!
    • 14.调用链路优化
      • 跨地域调用
      • 单元化架构:不同的用户路由到不同的集群单元
      • 微服务拆分过细会导致Rpc调用较多
      • 提前过滤,减少无效调用
      • 拆分接口
  • 相关优秀博客

常见思路

8.锁粒度避免过粗

synchronized

在高并发场景,为了防止超卖等情况,我们经常需要加锁来保护共享资源。但是,如果加锁的粒度过粗,是很影响接口性能的。

什么是加锁粒度呢?

其实就是就是你要锁住的范围是多大。比如你在家上卫生间,你只要锁住卫生间就可以了吧,不需要将整个家都锁起来不让家人进门吧,卫生间就是你的加锁粒度。

不管你是synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉及共享资源的,就不必要加锁。这就好像你上卫生间,不用把整个家都锁住,锁住卫生间门就可以了。

比如,在业务代码中,有一个ArrayList因为涉及到多线程操作,所以需要加锁操作,假设刚好又有一段比较耗时的操作(代码中的slowNotShare方法)不涉及线程安全问题。反例加锁,就是一锅端,全锁住:

//不涉及共享资源的慢方法
private void slowNotShare() {try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {}
}//错误的加锁方法
public int wrong() {long beginTime = System.currentTimeMillis();IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {//加锁粒度太粗了,slowNotShare其实不涉及共享资源synchronized (this) {slowNotShare();data.add(i);}});log.info("cosume time:{}", System.currentTimeMillis() - beginTime);return data.size();
}

正例:

public int right() {long beginTime = System.currentTimeMillis();IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {slowNotShare();//可以不加锁//只对List这部分加锁synchronized (data) {data.add(i);}});log.info("cosume time:{}", System.currentTimeMillis() - beginTime);return data.size();
}

对于锁的更细致使用来说,在java中提供了synchronized关键字给我们的代码加锁。

通常有两种写法:在方法上加锁在代码块上加锁

先看看如何在方法上加锁:

public synchronized doSave(String fileUrl) {mkdir();uploadFile(fileUrl);sendMessage(fileUrl);
}

这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。

但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。

我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。

这时,我们可以改成在代码块上加锁了,具体代码如下:

public void doSave(String path,String fileUrl) {synchronized(this) {if(!exists(path)) {mkdir(path);}}uploadFile(fileUrl);sendMessage(fileUrl);
}

这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。

最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。

当然,这种做在单机版的服务中,是没有问题的。但现在部署的生产环境,为了保证服务的稳定性,一般情况下,同一个服务会被部署在多个节点中。

同时它也带来了新的问题:synchronized只能保证一个节点加锁是有效的,但如果有多个节点如何加锁呢?

这就需要使用:分布式锁了。目前主流的分布式锁包括:redis分布式锁 和 数据库分布式锁。

redis分布式锁

在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。

public void doSave(String path,String fileUrl) {try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(!exists(path)) {mkdir(path);uploadFile(fileUrl);sendMessage(fileUrl);}return true;}} finally{unlock(lockKey,requestId);}  return false;
}

跟之前使用synchronized关键字加锁时一样,这里锁的范围也太大了,换句话说就是锁的粒度太粗,这样会导致整个方法的执行效率很低。

其实只有创建目录的时候,才需要加分布式锁,其余代码根本不用加锁。

于是,我们需要优化一下代码:

public void doSave(String path,String fileUrl) {if(this.tryLock()) {mkdir(path);}uploadFile(fileUrl);sendMessage(fileUrl);
}private boolean tryLock() {try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {return true;}} finally{unlock(lockKey,requestId);}  return false;
}

上面代码将加锁的范围缩小了,只有创建目录时才加了锁。这样看似简单的优化之后,接口性能能提升很多。

但是Redis锁也存在着一些弊端情况,如下共有八条总结性建议:

非原子操作

使用redis的分布式锁,我们首先想到的可能是setNx命令。

if (jedis.setnx(lockKey, val) == 1) {jedis.expire(lockKey, timeout);
}

这段代码确实可以加锁成功,但你有没有发现什么问题?

加锁操作和后面的设置超时时间是分开的,并非原子操作

假如加锁成功,但是设置超时时间失败了,该lockKey就变成永不失效。假如在高并发场景中,有大量的lockKey加锁成功了,但不会失效,有可能直接导致redis内存空间不足。

忘了释放锁

上面说到使用setNx命令加锁操作和设置超时时间是分开的,并非原子操作。

而在redis中还有set命令,该命令可以指定多个参数。

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {return true;
}
return false;

其中:

  • lockKey:锁的标识
  • requestId:请求id
  • NX:只在键不存在时,才对键进行设置操作。
  • PX:设置键的过期时间为 millisecond 毫秒。
  • expireTime:过期时间

set命令是原子操作,加锁和设置超时时间,一个命令就能轻松搞定。

使用set命令加锁,表面上看起来没有问题。但如果仔细想想,加锁之后,每次都要达到了超时时间才释放锁,会不会有点不合理?加锁后,如果不及时释放锁,会有很多问题。

分布式锁更合理的用法是:

  1. 手动加锁
  2. 业务操作
  3. 手动释放锁
  4. 如果手动释放锁失败了,则达到超时时间,redis会自动释放锁。

在这里插入图片描述

那么问题来了,如何释放锁呢?

try{String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {return true;}return false;
} finally {unlock(lockKey);
}  

只在finally中释放锁,就够了吗?

释放了别人的锁

在多线程场景中,可能会出现释放了别人的锁的情况。

假如线程A和线程B,都使用lockKey加锁。线程A加锁成功了,但是由于业务功能耗时时间很长,超过了设置的超时时间。这时候,redis会自动释放lockKey锁。此时,线程B就能给lockKey加锁成功了,接下来执行它的业务操作。恰好这个时候,线程A执行完了业务功能,释放了锁lockKey。这不就出问题了,线程B的锁,被线程A释放了。

那么,如何解决这个问题呢?

不知道你们注意到没?在使用set命令加锁时,除了使用lockKey锁标识,还多设置了一个参数:requestId,为什么要需要记录requestId呢?

答:requestId是在释放锁的时候用的。

if (jedis.get(lockKey).equals(requestId)) {jedis.del(lockKey);return true;
}
return false;

在释放锁的时候,先获取到该锁的值(之前设置值就是requestId),然后判断跟之前设置的值是否相同,如果相同才允许删除锁,返回成功。如果不同,则直接返回失败。

换句话说就是:自己只能释放自己加的锁,不允许释放别人加的锁。

当然在这里也需要保证 判断 和 删除的原子性问题

自旋锁

上面的加锁方法看起来好像没有问题,但如果你仔细想想,如果有1万的请求同时去竞争那把锁,可能只有一个请求是成功的,其余的9999个请求都会失败。

此外,还有一种场景:

比如,有两个线程同时上传文件到sftp,上传文件前先要创建目录。假设两个线程需要创建的目录名都是当天的日期,比如:20210920,如果不做如何控制,这样直接并发的创建,第二个线程会失败。

有同学会说:这还不容易,加一个redis分布式锁就能解决问题了,此外再判断一下,如果目录已经存在就不创建,只有目录不存在才需要创建。

try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(!exists(path)) {mkdir(path);}return true;}
} finally{unlock(lockKey,requestId);
}  
return false;

只是加redis分布式锁是不够的,因为第二个请求如果加锁失败了,接下来,是返回失败呢?还是返回成功呢?

在这里插入图片描述

显然肯定是不能返回失败的,如果返回失败了,这个问题还是没有被解决。如果文件还没有上传成功,直接返回成功会有更大的问题。头疼,到底该如何解决呢?

答:使用自旋锁

try {Long start = System.currentTimeMillis();while(true) {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(!exists(path)) {mkdir(path);}return true;}long time = System.currentTimeMillis() - start;if (time>=timeout) {return false;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}
} finally{unlock(lockKey,requestId);
}  
return false;

在规定的时间,比如500毫秒内,自旋不断尝试加锁(说白了,就是在死循环中,不断尝试加锁),如果成功则直接返回。如果失败,则休眠50毫秒,再发起新一轮的尝试。如果到了超时时间,还未加锁成功,则直接返回失败。

锁重入问题

我们都知道redis分布式锁是互斥的。如果我们对某个key加锁了,如果该key对应的锁还没失效,再用相同key去加锁,大概率会失败。

假设在某个请求中,需要获取一颗满足条件的菜单树或者分类树。我们以菜单为例,这就需要在接口中从根节点开始,递归遍历出所有满足条件的子节点,然后组装成一颗菜单树。

需要注意的是菜单不是一成不变的,在后台系统中运营同学可以动态添加、修改和删除菜单。为了保证在并发的情况下,每次都可能获取最新的数据,这里可以加redis分布式锁。

加redis分布式锁的思路是对的。但接下来问题来了,在递归方法中递归遍历多次,每次都是加的同一把锁。递归第一层当然是可以加锁成功的,但递归第二层、第三层…第N层,不就会加锁失败了?

递归方法中加锁的伪代码如下:

private int expireTime = 1000;public void fun(int level,String lockKey,String requestId){try{String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(level<=10){this.fun(++level,lockKey,requestId);} else {return;}}return;} finally {unlock(lockKey,requestId);}
}

如果你直接这么用,看起来好像没有问题。但最终执行程序之后发现,等待你的结果只有一个:出现异常

因为从根节点开始,第一层递归加锁成功,还没释放说,就直接进入第二层递归。因为requestId作为key的锁已经存在,所以第二层递归大概率会加锁失败,然后返回到第一层。第一层接下来正常释放锁,然后整个递归方法直接返回了。

那么这个问题该如何解决呢?

答:使用可重入锁

我们以redisson框架为例,它的内部实现了可重入锁的功能。

伪代码如下:

private int expireTime = 1000;public void run(String lockKey) {RLock lock = redisson.getLock(lockKey);this.fun(lock,1);
}public void fun(RLock lock,int level){try{lock.lock(5, TimeUnit.SECONDS);if(level<=10){this.fun(lock,++level);} else {return;}} finally {lock.unlock();}
}

接下来,聊聊redisson可重入锁的实现原理。

加锁
  • 先判断如果锁名不存在,则加锁。
  • 然后判断判断如果锁名和requestId值都存在,则使用hincrby命令给该锁名和requestId值计数,每次都加1。注意一下,这里就是重入锁的关键,锁重入一次就加1。
  • 如果锁名存在,但值不是requestId,则返回过期时间。
解锁
  • 先判断如果锁名和requestId值不存在,则时间返回。
  • 如果锁名和requestId值存在,则重入锁减1。
  • 如果减1后,重入锁的value值还大于0,说明还有引用,则重试设置过期时间。
  • 如果减1后,重入锁的value值还等于0,则可以删除锁,然后发消息通知等待线程抢锁。
锁竞争问题

如果有大量写入的场景,使用普通的redis分布式锁是没有问题的。

但如果有些业务场景,写入的操作比较少,反而有大量读取的操作。直接使用普通的redis分布式锁,性能会不会不太好?

我们都知道,锁的粒度越粗,多个线程抢锁时竞争就越激烈,造成多个线程锁等待的时间也就越长,性能也就越差。

所以,提升redis分布式锁性能的第一步,就是要把锁的粒度变细。

读写锁

众所周知,加锁的目的是为了保证,在并发环境中读写数据的安全性,即不会出现数据错误或者不一致的情况。

但在绝大多数实际业务场景中,一般是读数据的频率远远大于写数据。而线程间的并发读操作是并不涉及并发安全问题,我们没有必要给读操作加互斥锁,只要保证读写、写写并发操作上锁是互斥的就行,这样可以提升系统的性能。

我们以redisson框架为例,它内部已经实现了读写锁的功能。

读锁的伪代码如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {rLock.lock();//业务操作
} catch (Exception e) {log.error(e);
} finally {rLock.unlock();
}

写锁的伪代码如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {rLock.lock();//业务操作
} catch (InterruptedException e) {log.error(e);
} finally {rLock.unlock();
}

将读锁和写锁分开,最大的好处是提升读操作的性能,因为读和读之间是共享的,不存在互斥性。而我们的实际业务场景中,绝大多数数据操作都是读操作。所以,如果提升了读操作的性能,也就会提升整个锁的性能。

下面总结一个读写锁的特点:

  • 读与读是共享的,不互斥
  • 读与写互斥
  • 写与写互斥
锁分段

此外,为了减小锁的粒度,比较常见的做法是将大锁:分段

在java中ConcurrentHashMap,就是将数据分为16段,每一段都有单独的锁,并且处于不同锁段的数据互不干扰,以此来提升锁的性能。

class ConcurrentHashMap<K, V> {// 初始化数组,存放 Segmentprivate Segment[] segments;public ConcurrentHashMap(int initialCapacity) {segments = new Segment[16]; // 初始化为 16 个 Segmentfor (int i = 0; i < segments.length; i++) {segments[i] = new Segment();}}// 获取 Segmentprivate Segment segmentFor(int hash) {return segments[(segments.length - 1) & hash];}// 获取值public V get(K key) {int hash = hash(key);return segmentFor(hash).get(key, hash);}// 存入值public void put(K key, V value) {int hash = hash(key);segmentFor(hash).put(key, value, hash);}// Segment 类class Segment {// 使用 ReentrantLock 作为锁private final ReentrantLock lock = new ReentrantLock();// 存放键值对private Map<K, V> map = new HashMap<>();public V get(K key, int hash) {lock.lock();try {// 获取值return map.get(key);} finally {lock.unlock();}}public void put(K key, V value, int hash) {lock.lock();try {// 存入值map.put(key, value);} finally {lock.unlock();}}}
}

放在实际业务场景中,我们可以这样做:

比如在秒杀扣库存的场景中,现在的库存中有2000个商品,用户可以秒杀。为了防止出现超卖的情况,通常情况下,可以对库存加锁。如果有1W的用户竞争同一把锁,显然系统吞吐量会非常低。

为了提升系统性能,我们可以将库存分段,比如:分为100段,这样每段就有20个商品可以参与秒杀。

在秒杀的过程中,先把用户id获取hash值,然后除以100取模。模为1的用户访问第1段库存,模为2的用户访问第2段库存,模为3的用户访问第3段库存,后面以此类推,到最后模为100的用户访问第100段库存。

在这里插入图片描述

如此一来,在多线程环境中,可以大大的减少锁的冲突。以前多个线程只能同时竞争1把锁,尤其在秒杀的场景中,竞争太激烈了,简直可以用惨绝人寰来形容,其后果是导致绝大数线程在锁等待。现在多个线程同时竞争100把锁,等待的线程变少了,从而系统吞吐量也就提升了。

锁超时问题

前面提到过,如果线程A加锁成功了,但是由于业务功能耗时时间很长,超过了设置的超时时间,这时候redis会自动释放线程A加的锁。

通常我们加锁的目的是:为了防止访问临界资源时,出现数据异常的情况。比如:线程A在修改数据C的值,线程B也在修改数据C的值,如果不做控制,在并发情况下,数据C的值会出问题。

为了保证某个方法,或者段代码的互斥性,即如果线程A执行了某段代码,是不允许其他线程在某一时刻同时执行的,我们可以用synchronized关键字加锁。

但这种锁有很大的局限性,只能保证单个节点的互斥性。如果需要在多个节点中保持互斥性,就需要用redis分布式锁。

假设线程A加redis分布式锁的代码,包含代码1和代码2两段代码。

在这里插入图片描述

由于该线程要执行的业务操作非常耗时,程序在执行完代码1的时,已经到了设置的超时时间,redis自动释放了锁。而代码2还没来得及执行。

在这里插入图片描述

此时,代码2相当于裸奔的状态,无法保证互斥性。假如它里面访问了临界资源,并且其他线程也访问了该资源,可能就会出现数据异常的情况。(PS:我说的访问临界资源,不单单指读取,还包含写入)

那么,如何解决这个问题呢?

答:如果达到了超时时间,但业务代码还没执行完,需要给锁自动续期。

我们可以使用TimerTask类,来实现自动续期的功能:

Timer timer = new Timer(); 
timer.schedule(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//自动续期逻辑}
}, 10000, TimeUnit.MILLISECONDS);

获取锁之后,自动开启一个定时任务,每隔10秒钟,自动刷新一次过期时间。这种机制在redisson框架中,有个比较霸气的名字:watch dog,即传说中的看门狗

需要注意的地方是:在实现自动续期功能时,还需要设置一个总的过期时间,可以跟redisson保持一致,设置成30秒。如果业务代码到了这个总的过期时间,还没有执行完,就不再自动续期了。

自动续期的功能是获取锁之后开启一个定时任务,每隔10秒判断一下锁是否存在,如果存在,则刷新过期时间。如果续期3次,也就是30秒之后,业务方法还是没有执行完,就不再续期了。
主从复制的问题

如果redis存在多个实例。比如:做了主从,或者使用了哨兵模式,基于redis的分布式锁的功能,就会出现问题。

假设redis现在用的主从模式,1个master节点,3个slave节点。master节点负责写数据,slave节点负责读数据。

在这里插入图片描述

本来是和谐共处,相安无事的。redis加锁操作,都在master上进行,加锁成功后,再异步同步给所有的slave。

突然有一天,master节点由于某些不可逆的原因,挂掉了。

这样需要找一个slave升级为新的master节点,假如slave1被选举出来了。

在这里插入图片描述

果有个锁A比较悲催,刚加锁成功master就挂了,还没来得及同步到slave1。

这样会导致新master节点中的锁A丢失了。后面,如果有新的线程,使用锁A加锁,依然可以成功,分布式锁失效了。

那么,如果解决这个问题呢?

答:redisson框架为了解决这个问题,提供了一个专门的类:RedissonRedLock,使用了Redlock算法。

RedissonRedLock解决问题的思路如下:

  1. 需要搭建几套相互独立的redis环境,假如我们在这里搭建了3套。
  2. 每套环境都有一个redisson node节点。
  3. 多个redisson node节点组成了RedissonRedLock。
  4. 环境包含:单机、主从、哨兵和集群模式,可以是一种或者多种混合。

在这里我们以主从为例,架构图如下:

在这里插入图片描述

RedissonRedLock加锁过程如下:

  1. 循环向所有的redisson node节点加锁,假设节点数为N,例子中N等于5。
  2. 如果在N个节点当中,有N/2 + 1个节点加锁成功了,那么整个RedissonRedLock加锁是成功的。
  3. 如果在N个节点当中,小于N/2 + 1个节点加锁成功,那么整个RedissonRedLock加锁是失败的。
  4. 如果中途发现各个节点加锁的总耗时,大于等于设置的最大等待时间,则直接返回失败。

从上面可以看出,使用Redlock算法,确实能解决多实例场景中,假如master节点挂了,导致分布式锁失效的问题。

但也引出了一些新问题,比如:

  1. 需要额外搭建多套环境,申请更多的资源,需要评估一下,经费是否充足。
  2. 如果有N个redisson node节点,需要加锁N次,最少也需要加锁N/2+1次,才知道redlock加锁是否成功。显然,增加了额外的时间成本,有点得不偿失。

数据库分布式锁

基于数据库表的增删

基于数据库表增删是最简单的方式,首先创建一张锁的表主要包含下列字段:方法名,时间戳等字段。

具体使用的方法为:当需要锁住某个方法时,往该表中插入一条相关的记录。需要注意的是,方法名有唯一性约束。如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕,需要删除该记录。

基于数据库排他锁

我们还可以通过数据库的排他锁来实现分布式锁。基于 Mysql 的 InnoDB 引擎,可以使用以下方法来实现加锁操作:

public void lock(){connection.setAutoCommit(false)int count = 0;while(count < 4){try{select * from lock where lock_name=xxx for update;if(结果不为空){//代表获取到锁return;}}catch(Exception e){}//为空或者抛异常的话都表示没有获取到锁sleep(1000);count++;}throw new LockException();
}

在查询语句后面增加 for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。其他没有获取到锁的就会阻塞在上述 select 语句上,可能的结果有 2 种,在超时之前获取到了锁,在超时之前仍未获取到锁。

获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行业务逻辑,执行完业务之后释放锁。

9.切换存储方式:文件中转暂存数据

如果数据太大,落地数据库实在是慢的话,就可以考虑先用文件的方式暂存。先保存文件,再异步下载文件,慢慢保存到数据库

比如说一个转账接口,如果是并发开启,10个并发度,每个批次1000笔转账明细数据,数据库插入会特别耗时,大概6秒左右;这个跟我们公司的数据库同步机制有关,并发情况下,因为优先保证同步,所以并行的插入变成串行啦,就很耗时。

数据库同步机制可能导致并行的插入变成串行的原因有很多,下面列举了一些可能的情况:

  1. 锁竞争:当多个事务同时尝试向相同的数据页或数据行插入数据时,数据库系统可能会使用锁来确保数据的一致性。如果同步机制导致大量的锁竞争,那么并行插入操作可能会被迫等待其他事务释放锁,从而导致串行化。
  2. 同步点阻塞:某些数据库同步机制可能会引入同步点,要求所有的写操作都必须在这些同步点进行同步,这样就会导致并行的写操作变成串行化。
  3. 冲突检测与重试:在数据库同步的过程中,可能会发生数据冲突,系统需要检测并解决这些冲突。这种检测和解决过程可能会导致并行插入变成串行化,因为某些操作需要等待其他操作完成后才能执行。
  4. 数据复制延迟:如果数据库采用了主从复制或者集群复制的机制,数据同步可能会引入一定的延迟。在这种情况下,并行的插入操作可能会因为数据尚未完全同步而变成串行化。

优化前1000笔明细转账数据,先落地DB数据库,返回处理中给用户,再异步转账。如图:

在这里插入图片描述

记得当时压测的时候,高并发情况,这1000笔明细入库,耗时都比较大。所以我转换了一下思路,把批量的明细转账记录保存的文件服务器,然后记录一笔转账总记录到数据库即可。接着异步再把明细下载下来,进行转账和明细入库。最后优化后,性能提升了十几倍

优化后,流程图如下:

在这里插入图片描述

如果你的接口耗时瓶颈就在数据库插入操作这里,用来批量操作等,还是效果还不理想,就可以考虑用文件或者MQ等暂存。有时候批量数据放到文件,会比插入数据库效率更高。

10.优化程序结构

逻辑结构

优化程序逻辑、程序代码,是可以节省耗时的。比如,你的程序创建多不必要的对象、或者程序逻辑混乱,多次重复查数据库、又或者你的实现逻辑算法不是最高效的,等等。

我举个简单的例子:复杂的逻辑条件,有时候调整一下顺序,就能让你的程序更加高效。

假设业务需求是这样:如果用户是会员,第一次登陆时,需要发一条感谢短信。如果没有经过思考,代码直接这样写了

if(isUserVip && isFirstLogin){sendSmsMsg();
}

假设有5个请求过来,isUserVip判断通过的有3个请求,isFirstLogin通过的只有1个请求。那么以上代码,isUserVip执行的次数为5次,isFirstLogin执行的次数也是3次,如下:

在这里插入图片描述

如果调整一下isUserVipisFirstLogin的顺序:

if(isFirstLogin && isUserVip ){sendMsg();
}

isFirstLogin执行的次数是5次,isUserVip执行的次数是1次:

在这里插入图片描述

程序是不是变得更高效了呢?

日志

在高并发的查询场景下,打印日志可能导致接口性能下降的问题。

在排查问题时顺手打印了日志并且带上线。高峰期时发现接口的 tp99 耗时大幅增加,同时 CPU 负载和垃圾回收频率也明显增加,磁盘负载也增加很多。日志删除后,系统回归正常。

特别是在日志中包含了大数组或大对象时,更要谨慎,避免打印这些日志。

不打日志,无法有效排查问题。怎么办呢?

为了有效地排查问题,建议引入白名单机制。具体做法是,在打印日志之前,先判断用户是否在白名单中,如果不在,则不打印日志;如果在,则打印日志。通过将公司内的产品、开发和测试人员等相关同事加入到白名单中,有利于及时发现线上问题。当用户提出投诉时,也可以将相关用户添加到白名单,并要求他们重新操作以复现问题。

这种方法既满足了问题排查的需求,又避免了给线上环境增加压力。(在测试环境中,可以完全开放日志打印功能)

11.压缩传输内容

压缩传输内容,传输报文变得更小,因此传输会更快啦。10M带宽,传输10k的报文,一般比传输1M的会快呀。

打个比喻,一匹千里马,它驮着100斤的货跑得快,还是驮着10斤的货物跑得快呢?

再举个视频网站的例子:

如果不对视频做任何压缩编码,因为带宽又是有限的。巨大的数据量在网络传输的耗时会比编码压缩后,慢好多倍

压缩文本数据可以有效地减少该数据所需的存储空间,从而提高数据库和缓存的空间利用率。然而,压缩和解压缩的过程会增加CPU的负载,因此需要仔细考虑是否有必要进行数据压缩。此外,还需要评估压缩后数据的效果,即压缩对数据的影响如何。

比如说使用GZIP压缩算法的cpu负载和耗时都是比较高的。使用压缩非但不能起到降低接口耗时的效果,可能导致接口耗时增加,要谨慎使用。除此之外,还有其他压缩算法在压缩时间和压缩率上有所权衡。可以选择适合的自己的压缩算法。

12.线程池设计

我们使用线程池,就是让任务并行处理,更高效地完成任务。但是有时候,如果线程池设计不合理,接口执行效率则不太理想。

一般我们需要关注线程池的这几个参数:核心线程、最大线程数量、阻塞队列

  • 如果核心线程过小,则达不到很好的并行效果。
  • 如果阻塞队列不合理,不仅仅是阻塞的问题,甚至可能会OOM
  • 如果线程池不区分业务隔离,有可能核心业务被边缘业务拖垮

下面是线程池设计建议总结:

线程池默认使用无界队列,任务过多导致OOM

JDK开发者提供了线程池的实现类,我们基于Executors组件,就可以快速创建一个线程池。日常工作中,一些小伙伴为了开发效率,反手就用Executors新建个线程池。写出类似以下的代码:

public class NewFixedTest {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < Integer.MAX_VALUE; i++) {executor.execute(() -> {try {Thread.sleep(10000);} catch (InterruptedException e) {//do nothing}});}}
}

使用newFixedThreadPool创建的线程池,是会有坑的,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。运行一下以上代码,出现了OOM

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceededat java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)at com.example.dto.NewFixedTest.main(NewFixedTest.java:14)

这是因为newFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo代码设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM

看下newFixedThreadPool的相关源码,是可以看到一个无界的阻塞队列的,如下:

//阻塞队列是LinkedBlockingQueue,并且是使用的是无参构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}//无参构造函数,默认最大容量是Integer.MAX_VALUE,相当于无界的阻塞队列的了
public LinkedBlockingQueue() {this(Integer.MAX_VALUE);
}

因此,工作中,建议大家自定义线程池,并使用指定长度的阻塞队列

线程池创建线程过多,导致OOM

有些小伙伴说,既然Executors组件创建出的线程池newFixedThreadPool,使用的是无界队列,可能会导致OOM。那么,Executors组件还可以创建别的线程池,如newCachedThreadPool,我们用它也不行嘛?

我们可以看下newCachedThreadPool的构造函数:

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

它的最大线程数是Integer.MAX_VALUE。大家应该意识到使用它,可能会引发什么问题了吧。没错,如果创建了大量的线程也有可能引发OOM

所以我们使用线程池的时候,还要当心线程创建过多,导致OOM问题。大家尽量不要使用newCachedThreadPool,并且如果自定义线程池时,要注意一下最大线程数。

共享线程池,次要逻辑拖垮主要逻辑

要避免所有的业务逻辑共享一个线程池。比如你用线程池A来做登录异步通知,又用线程池A来做对账。如下图:

在这里插入图片描述

如果对账任务checkBillService响应时间过慢,会占据大量的线程池资源,可能直接导致没有足够的线程资源去执行loginNotifyService的任务,最后影响登录。就这样,因为一个次要服务,影响到重要的登录接口,显然这是绝对不允许的。因此,我们不能将所有的业务一锅炖,都共享一个线程池,因为这样做,风险太高了,犹如所有鸡蛋放到一个篮子里。应当做线程池隔离

在这里插入图片描述

线程池拒绝策略的坑,使用不当导致阻塞

我们知道线程池主要有四种拒绝策略,如下:

  • AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。(默认拒绝策略)
  • DiscardPolicy:丢弃任务,但是不抛出异常。
  • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。
  • CallerRunsPolicy:由调用方线程处理该任务。

如果线程池拒绝策略设置不合理,就容易有坑。我们把拒绝策略设置为DiscardPolicy或DiscardOldestPolicy并且在被拒绝的任务,Future对象调用get()方法,那么调用线程会一直被阻塞。

温馨提示,日常开发中,使用 Future.get() 时,尽量使用带超时时间的,因为它是阻塞的。

future.get(1, TimeUnit.SECONDS);

Spring内部线程池的坑

工作中,个别开发者,为了快速开发,喜欢直接用spring@Async,来执行异步任务。

@Async
public void testAsync() throws InterruptedException {System.out.println("处理异步任务");TimeUnit.SECONDS.sleep(new Random().nextInt(100));
}

Spring内部线程池,其实是SimpleAsyncTaskExecutor,这玩意有点坑,它不会复用线程的,它的设计初衷就是执行大量的短时间的任务。

也就是说来了一个请求,就会新建一个线程!大家使用spring@Async时,要避开这个坑,自己再定义一个线程池。正例如下:

@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setThreadNamePrefix("tianluo-%d");// 其他参数设置return new ThreadPoolTaskExecutor();
}

使用线程池时,没有自定义命名

使用线程池时,如果没有给线程池一个有意义的名称,将不好排查回溯问题。这不算一个坑吧,只能说给以后排查埋坑

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20));executorOne.execute(()->{throw new NullPointerException();});}
}

运行结果:

Exception in thread "pool-1-thread-1" java.lang.NullPointerExceptionat com.example.dto.ThreadTest.lambda$main$0(ThreadTest.java:17)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)

可以发现,默认打印的线程池名字是pool-1-thread-1,如果排查问题起来,并不友好。因此建议大家给自己线程池自定义个容易识别的名字。其实用CustomizableThreadFactory即可,正例如下:

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool"));executorOne.execute(()->{throw new NullPointerException();});}
}

线程池参数设置不合理

线程池最容易出坑的地方,就是线程参数设置不合理。比如核心线程设置多少合理,最大线程池设置多少合理等等。当然,这块不是乱设置的,需要结合具体业务

比如线程池如何调优,如何确认最佳线程数?

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。

线程池异常处理的坑

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool"));for (int i = 0; i < 5; i++) {executorOne.submit(()->{System.out.println("current thread name" + Thread.currentThread().getName());Object object = null;System.out.print("result## " + object.toString());});}}
}

按道理,运行这块代码应该抛空指针异常才是的,对吧。但是,运行结果却是这样的;

current thread nameTianluo-Thread-pool1
current thread nameTianluo-Thread-pool2
current thread nameTianluo-Thread-pool3
current thread nameTianluo-Thread-pool4
current thread nameTianluo-Thread-pool5

这是因为使用submit提交任务,不会把异常直接这样抛出来。最好就是try...catch捕获

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool"));for (int i = 0; i < 5; i++) {executorOne.submit(()->{System.out.println("current thread name" + Thread.currentThread().getName());try {Object object = null;System.out.print("result## " + object.toString());}catch (Exception e){System.out.println("异常了"+e);}});}}
}

也可以使用Future.get来获取异常。

线程池使用完毕后,忘记关闭

如果线程池使用完,忘记关闭的话,有可能会导致内存泄露问题。所以,大家使用完线程池后,记得关闭一下。同时,线程池最好也设计成单例模式,给它一个好的命名,以方便排查问题。

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Tianluo-Thread-pool"));executorOne.execute(() -> {});//关闭线程池executorOne.shutdown();}
}

ThreadLocal与线程池搭配,线程复用,导致信息错乱

使用ThreadLocal缓存信息,如果配合线程池一起,有可能出现信息错乱的情况。先看下一下例子:

private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before  = Thread.currentThread().getName() + ":" + currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息String after  = Thread.currentThread().getName() + ":" + currentUser.get();//汇总输出两次查询结果Map result = new HashMap();result.put("before", before);result.put("after", after);return result;
}

按理说,每次获取的before应该都是null,但是呢,程序运行在 Tomcat 中,执行程序的线程是Tomcat的工作线程,而Tomcat的工作线程是基于线程池的。

线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。

把tomcat的工作线程设置为1

server.tomcat.max-threads=1

用户1,请求过来,会有以下结果,符合预期:

在这里插入图片描述

用户2请求过来,会有以下结果,「不符合预期」:

在这里插入图片描述

因此,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据,正例如下:

@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {String before  = Thread.currentThread().getName() + ":" + currentUser.get();currentUser.set(userId);try {String after = Thread.currentThread().getName() + ":" + currentUser.get();Map result = new HashMap();result.put("before", before);result.put("after", after);return result;} finally {//在finally代码块中删除ThreadLocal中的数据,确保数据不串currentUser.remove();}
}

13.机器问题 (GC、线程打满、太多IO资源没关闭等等)

有时候,我们的接口慢,就是机器处理问题。主要有fullGC、线程打满、太多IO资源没关闭等等。

GC

比如说要导出60W+的excel的时候,卡死了,接着收到了监控告警。排查得出,代码是Apache POI生成的excel,导出excel数据量很大时,当时JVM内存吃紧会直接Full GC了。

无论是Young GC还是Full GC,在进行垃圾回收时都会暂停所有的业务线程。因此,需要关注垃圾回收的频率,以确保对业务的影响尽可能小。

一般情况下,通过调整堆大小和新生代大小可以解决大部分垃圾回收问题。其中,新生代是用于存放新创建的对象的区域。对于Young GC的频率增加的情况,一般是系统的请求量大量增长导致。但如果young gc增长非常多,就需要考虑是否需要增加新生代的大小。

因为如果新生代过小,很容易被打满。这导致本可以被Young GC掉的对象被晋升(Promotion)到老年代,过早地进入老年代。这样一来,不仅Young GC频繁触发,Full GC也会频繁触发。

线程

如果线程打满了,也会导致接口都在等待了。所以。如果是高并发场景,我们需要接入限流,把多余的请求拒绝掉

资源

如果IO资源没关闭,也会导致耗时增加。这个大家可以看下,平时你的电脑一直打开很多很多文件,是不是会觉得很卡。

提升服务器硬件

如果cpu负载较高 可以考虑提高每个实例cpu数量,提高实例个数。同时关注网络IO负载,如果机器流量较大,网卡带宽可能成为瓶颈。

高峰期和低峰期如果机器负载相差较大,可以考虑设置弹性伸缩策略,高峰期之前自动扩容,低峰期自动缩容,最大程度提高资源利用率。

关于JVM调优部分的内容,将会在后续专门的出一些文章,因为目前笔者对这方面理解还不够,所以暂不多做赘述!!!

14.调用链路优化

在评估接口性能时,我们需要首先找出最耗时的部分,并优化它,这样优化效果才会立竿见影。

跨地域调用

假如说北京到上海的跨地域调用需要耗费大约30毫秒的时间,这个耗时是相当高的,所以我们应该特别关注调用链路上是否存在跨地域调用的情况。这些跨地域调用包括Rpc调用、Http调用、数据库调用、缓存调用以及MQ调用等等。在整理调用链路的时候,我们还应该标注出跨地域调用的次数,例如跨地域调用数据库可能会出现多次,在链路上我们需要明确标记。我们可以考虑通过降低调用次数来提高性能,因此在设计优化方案时,我们应该特别关注如何减少跨地域调用的次数。

举个例子,在某种情况下,假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。

单元化架构:不同的用户路由到不同的集群单元

如果主数据库位于北京,那么南方的用户每次写请求就只能通过跨地域访问来完成吗?实际上并非如此。数据库的主库不仅可以存在于一个地域,而是可以在多个地域上部署主数据库。将每个用户归属于最近的地域,该用户的请求都会被路由到所在地域的数据库。这样的部署不仅提升了系统性能,还提高了系统的容灾等级,即使单个机房发生故障也不会影响全网的用户。

这个思想类似于CDN(内容分发网络),它能够将用户请求路由到最近的节点。事实上,由于用户的存储数据已经在该地域的数据库中,用户的请求极少需要切换到其他地域。

为了实现这一点,我们需要一个用户路由服务来提供用户所在地域的查询,并且能够提供高并发的访问。

除了数据库之外,其他的存储中间件(如MQ、Redis等)以及Rpc框架都需要具备单元化架构能力。

微服务拆分过细会导致Rpc调用较多

微服务拆分过细会导致更多的RPC调用,一次简单的请求可能就涉及四五个服务,当访问量非常高时,多出来的三五次Rpc调用会导致接口耗时增加很多。

每个服务都需要处理网络IO,序列化反序列化,服务的GC 也会导致耗时增加,这样算下来一个大服务的性能往往优于5个微服务。

当然服务过于臃肿会降低开发维护效率,也不利于技术升级。微服务过多也有问题,例如增加整体链路耗时、基础架构升级工作量变大、单个需求代码变更的服务更多等弊端。需要你权衡开发效率、线上性能、领域划分等多方面因素。

提前过滤,减少无效调用

在某些活动匹配的业务场景里,相当多的请求实际上是不满足条件的,如果能尽早的过滤掉这些请求,就能避免很多无效查询。例如用户匹配某个活动时,会有非常多的过滤条件,如果该活动的特点是仅少量用户可参加,那么可首先使用人群先过滤掉大部分不符合条件的用户。

拆分接口

前面提到如果Http接口功能过于庞大,核心数据和非核心数据杂糅在一起,耗时高和耗时低的数据耦合在一起。为了优化请求的耗时,可以通过拆分接口,将核心数据和非核心数据分别处理,从而提高接口的性能。

而在Rpc接口方面,也可以使用类似的思路进行优化。当上游需要调用多个Rpc接口时,可以并行地调用这些接口。优先返回核心数据,如果处理非核心数据或者耗时高的数据超时,则直接降级,只返回核心数据。这种方式可以提高接口的响应速度和效率,减少不必要的等待时间。

相关优秀博客

后端接口性能优化分析-问题发现&问题定义-CSDN博客

后端接口性能优化分析-多线程优化-CSDN博客

后端接口性能优化分析-数据库优化-CSDN博客

相关文章:

后端接口性能优化分析-程序结构优化

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…...

【SpringBoot3+Vue3】三【实战篇】-后端(优化)

目录 一、登录优化-redis 1、SpringBoot集成redis 1.1 pom 1.2 yml 1.3 测试程序&#xff08;非必须&#xff09; 1.4 启动redis&#xff0c;执行测试程序 2、令牌主动失效&#xff08;代码优化&#xff09; 2.1 UserController设置token到redis 2.2 登录拦截器Log…...

DevExpress中文教程 - 如何在macOS和Linux (CTP)上创建、修改报表(上)

DevExpress Reporting是.NET Framework下功能完善的报表平台&#xff0c;它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集&#xff0c;包括数据透视表、图表&#xff0c;因此您可以构建无与伦比、信息清晰的报表。 DevExpress Reports — 跨平台报表组件&#x…...

一个iOS tableView 滚动标题联动效果的实现

效果图 情景 tableview 是从屏幕顶部开始的&#xff0c;现在有导航栏&#xff0c;和栏目标题视图将tableView的顶部覆盖了 分析 我们为了达到滚动到某个分区选中标题的效果&#xff0c;就得知道 展示最顶部的cell或者区头在哪个分区范围内 所以我们必须首先获取顶部的位置 …...

代码执行相关函数以及简单例题

代码/命令 执行系列 相关函数 &#xff08;代码注入&#xff09;...

大数据爬虫分析基于Python+Django旅游大数据分析系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 基于Python和Django的旅游大数据分析系统是一种使用Python编程语言和Django框架开发的系统&#xff0c;用于处理和分…...

C# 结构体介绍

文章目录 定义结构体实例化结构体结构体的值类型特性结构体和类的区别限制 C# 中的结构体&#xff08;Struct&#xff09;是一种值类型数据结构&#xff0c;用于封装不同或相同类型的数据成一个单一的实体。结构体非常适合用来表示轻量级的对象&#xff0c;比如坐标点、颜色值或…...

【机器学习】特征工程:特征预处理,归一化、标准化、处理缺失值

特征预处理采用的是特定的统计方法&#xff08;数学方法&#xff09;将数据转化为算法要求的数字 1. 数值型数据 归一化&#xff0c;将原始数据变换到[0,1]之间 标准化&#xff0c;数据转化到均值为0&#xff0c;方差为1的范围内 缺失值&#xff0c;缺失值处理成均值、中…...

Pytorch torch.norm函数详解用法

torch.norm参数定义 torch版本1.6 def norm(input, p"fro", dimNone, keepdimFalse, outNone, dtypeNone)input input (Tensor): the input tensor 输入为tensorp p (int, float, inf, -inf, fro, nuc, optional): the order of norm. Default: froThe following …...

【DevOps】Git 图文详解(二):Git 安装及配置

Git 图文详解&#xff08;二&#xff09;&#xff1a;Git 安装及配置 1.Git 的配置文件2.配置 - 初始化用户3.配置 - 忽略.gitignore Git 官网&#xff1a;https://www.git-scm.com/ 下载安装包进行安装。Git 的使用有两种方式&#xff1a; 命令行&#xff1a;Git 的命令通过系…...

亚马逊美国站CPC认证ASTM F963测试项目要求有哪些?

ASTM F963是美国材料和试验联合会&#xff08;ASTM&#xff09;制定的儿童玩具安全性的标准规范&#xff0c;专门针对儿童玩具产品的安全性进行了规定和要求。 ASTM F963标准的内容和要求包括&#xff1a; 1、物理机械性能&#xff1a;规定了玩具的物理机械性能要求&#xff0…...

通付盾Web3专题 | KYT/AML:Web3合规展业的必要条件

与传统证券一样&#xff0c;基于区块链技术发展出来的虚拟资产交易所经历了快速发展而缺乏有效监管的行业早期。除了科技光环加持的各种区块链项目方、造富神话之外&#xff0c;交易所遭到黑客攻击、内部偷窃作恶、甚至经营主体异常而致使投资人血本无归的案例亦令人触目惊心。…...

Centos8配置Zabbix5.0中文汉化

1.点击【Sign in】按钮&#xff0c;输入用户名和密码进入Zabbix的首页&#xff0c;结果如图。 2.点击左边导航栏的【User settings】链接&#xff0c;进入用户个性化设置界面&#xff0c;结果如图。 3.在搭建Zabbix的虚拟机上使用yum命令下载中文包。 yum install glibc-langpa…...

元数据管理,数字化时代企业的基础建设

随着新一代信息化、数字化技术的应用&#xff0c;众多领域通过科技革命和产业革命实现了深度化的数字改造&#xff0c;进入到以数据为核心驱动力的&#xff0c;全新的数据处理时代&#xff0c;并通过业务系统、商业智能BI等数字化技术和应用实现了数据价值&#xff0c;从数字经…...

大数据之Hive:regexp_extract函数案例

目录 一、正则的通配符简介1、正则表达式的符号及意义2、各种操作符的运算优先级&#xff1a; 二、案例数据要求分析实现输出结果实现2实现3 总结 一、正则的通配符简介 1、正则表达式的符号及意义 符号含义实列/做为转意&#xff0c;即通常在"/"后面的字符不按原来…...

tsconfig.json无法写入文件“XXXX“因为它会覆盖输入文件

在开发ts项目的时候&#xff0c;包错提示无法写入文件&#xff1a; tsconfig.json无法写入文件"XXXX"因为它会覆盖输入文件 这是tsconfig.json文件配置问题&#xff0c;需要加入下面的配置就好了&#xff1a; {"compilerOptions": {"outDir": …...

本周Github有趣项目:draw-a-ui等

有趣的项目、工具和库 gpt-crawler 抓取网站以生成知识文件&#xff0c;从而从 URL 创建您自己的自定义 GPT。 需要步骤&#xff1a; 配置运行爬虫、 将您的数据上传到 OpenAI&#xff1a;使用此选项通过 UI 访问您生成的知识&#xff0c;您可以轻松与他人共享 创建自定义助…...

VBA如何快速识别Excel单元格中的文本数字

Excel中一种非常特殊的数字&#xff0c;这些数字看似数字&#xff0c;其实是文本格式&#xff08;下文简称为文本数字&#xff09;&#xff0c;在单元格的左上角会有一个绿色小三角作为标志&#xff0c;如B1:B3单元格。 在编程时为什么需要区分普通数字和文本数字呢&#xff…...

Mysql数据库 16.SQL语言 数据库事务

一、数据库事务 数据库事务介绍——要么全部成功要么全部失败 我们把完成特定的业务的多个数据库DML操作步骤称之为一个事务 事务——就是完成同一个业务的多个DML操作 例&#xff1a; 数据库事务四大特性 原子性&#xff08;A&#xff09;&#xff1a;一个事务中的多个D…...

docker 部署Redis集群(三主三从,以及扩容、缩容)

1&#xff1a;创建6个redis容器 docker run -d --name redis01 --net host --privilegedtrue -v /opt/redis/redis01:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381 docker run -d --name redis02 --net host --privilegedtrue -v /opt/redis/redis0…...

JavaScript 浮点数运算的精度问题及解决

JavaScript 浮点数运算的精度问题及解决 在 JavaScript 中整数和浮点数都属于 Number 数据类型&#xff0c;当浮点数做数学运算的时候&#xff0c;你经常会发现一些问题&#xff0c;举几个例子&#xff1a; 0.1 0.2 0.30000000000000004 console.log(0.1 0.2) 0.3000000…...

基于STM32的无线传感器网络(WSN)通信方案设计与实现

无线传感器网络&#xff08;Wireless Sensor Network&#xff0c;简称WSN&#xff09;是由一组分布式的无线传感器节点组成的网络&#xff0c;用于监测和收集环境中的各类物理信息。本文将基于STM32微控制器&#xff0c;设计并实现一个简单的无线传感器网络通信方案&#xff0c…...

Flink和Kafka连接时的精确一次保证

Flink写入Kafka两阶段提交 端到端的 exactly-once&#xff08;精准一次&#xff09; kafka -> Flink -> kafka 1&#xff09;输入端 输入数据源端的 Kafka 可以对数据进行持久化保存&#xff0c;并可以重置偏移量&#xff08;offset&#xff09; 2&#xff09;Flink内…...

UE4动作游戏实例RPG Action解析三:实现效果,三连击Combo,射线检测,显示血条,火球术

一、三连Combo 实现武器三连击,要求: 1.下一段Combo可以随机选择, 2.在一定的时机才能再次检测输入 3. 等当前片段播放完才播放下一片段 1.1、蒙太奇设置 通过右键-新建蒙太奇片段,在蒙太奇里创建三个片段,并且移除相关连接,这样默认只会播放第一个片段 不同片段播…...

Linux/麒麟系统上部署Vue+SpringBoot前后端分离项目

目录 1. 前端准备工作 1.1 在项目根目录创建两份环境配置文件 1.2 环境配置 2. 后端准备工作 2.1 在项目resources目录创建两份环境配置文件 2.2 环境配置 3. 前后端打包 3.1 前端打包 3.2 后端打包 4、服务器前后端配置及部署 4.1 下载、安装、启动Nginx 4.2 前端项目部署…...

STM32在FreeRTOS下的us延时

STM32在FreeRTOS下的us延时 前言 freeRTOS下跑SPI时需要微秒级别的延时&#xff0c;但是freeRTOS只提供了毫秒级的&#xff0c;记录一下实现us延时的方法。 前期分析 最简单的方式就是开个定时器或者干脆直接计算一下用nop做都可以实现us延时&#xff0c;但是显然还是使用滴…...

软件测试/人工智能丨深入人工智能软件测试:PyTorch引领新时代

在人工智能的浪潮中&#xff0c;软件测试的角色变得愈发关键。本文将介绍在人工智能软件测试中的一些关键技术&#xff0c;以及如何借助PyTorch深度学习框架来推动测试的创新与升级。 PyTorch&#xff1a;深度学习的引擎 PyTorch作为一种开源的深度学习框架&#xff0c;为软件…...

Android 当中的 Fragment 协作解耦方式

Android 当中的 Fragment 协作解耦方式 文章目录 Android 当中的 Fragment 协作解耦方式第一章 前言介绍第01节 遇到的问题第02节 绘图说明 第二章 核心代码第01节 代理人接口第02节 中间人 Activity第03节 开发者A第04节 开发者B第05节 测试类 第一章 前言介绍 第01节 遇到的…...

城市网吧视频智能监控方案,实现视频远程集中监控

网吧环境较为复杂&#xff0c;电脑设备众多且人员流动性大&#xff0c;极易发生人员或消防事故&#xff0c;亟需改变&#xff0c;TSINGSEE青犀AI智能网吧视频监管方案可以帮助实现对网吧环境和用户活动的实时监控和管理。 1、视频监控系统 在网吧内部布置高清摄像头&#xff0…...

C#WPF视频播放器实例

本文实例演示C#WPF视频播放器 实例如下: 修改mainwindow的代码 <Windowx:Class="PlayerDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xml…...

【uniapp】Google Maps

话不多说 直接上干货 提前申请谷歌地图账号一、新建地图 使用h5获取当前定位或者使用三方uniapp插件 var coords ""navigator.geolocation.getCurrentPosition(function(position) {coords {lat: position.coords.latitude,lng: position.coords.longitude};lats …...

C语言变量与常量

跟着肯哥&#xff08;不是我&#xff09;学C语言的变量和常量、跨文件访问、栈空间 栈空间还不清楚&#xff0c;期待明天的课程内容 C变量 变量&#xff08;Variable&#xff09;是用于存储和表示数据值的名称。 主要包括四个环节&#xff1a;定义、初始化、声明、使用 在我刚…...

AI创作系统ChatGPT网站源码/支持DALL-E3文生图/支持最新GPT-4-Turbo模型+Prompt应用

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…...

二维码智慧门牌管理系统升级,异常门牌聚合解决方案助力高效管理

文章目录 前言一、异常门牌聚合解决方案 前言 在今天的数字化时代&#xff0c;智慧城市已成为发展趋势&#xff0c;其中二维码智慧门牌管理系统扮演着至关重要的角色。通过对门牌信息进行数字化管理&#xff0c;该系统极大提升了城市管理的效率和便捷性。然而&#xff0c;随着…...

【XTDrone Ubuntu20.04】XTDrone+ Ubuntu20.04 + PX4安装

XTDrone仿真平台配置 文章目录 XTDrone仿真平台配置依赖安装 ROS一键安装Marvos安装PX4 安装安装QTGroundControlXTDrone下载安装 环境&#xff1a; VMWare 16.0 Ubuntu 22.04 &#xff08;因为没人配过&#xff09;Ubuntu 20.04 参考文章&#xff1a; 仿真平台基础配置 (yuq…...

河北大学选择ZStack Cube超融合一体机打造实训云平台

河北大学通过云轴科技ZStack Cube超融合一体机构建校园实训云平台&#xff0c;部署测试仅耗时1天&#xff0c;该平台能够更快地为学生提供高性能、高可用的云主机、云存储和云网络服务&#xff1b;同时也能满足日常运维管理要求&#xff0c;为学生提供更好的实训环境。 河北省…...

IDEA远程一键部署SpringBoot到Docker

IDEA是Java开发利器&#xff0c;Spring Boot是Java生态中最流行的微服务框架&#xff0c;docker是时下最火的容器技术&#xff0c;那么它们结合在一起会产生什么化学反应呢&#xff1f; 一、开发前准备 1. Docker安装 可以参考&#xff1a;https://docs.docker.com/install/ 2…...

索引三星结构

三星索引的定义&#xff0c;可以先给我们对索引优化提供一个大概的思路&#xff1a; 满足第1颗星&#xff1a; 取出所有的等值谓词的列&#xff0c;作为索引最开头的列——以任意顺序都可以。 满足第2颗星&#xff1a; 将order by加入到索引列&#xff0c;不要改变这些列的顺…...

rust 笔记 高级错误处理

文章目录 错误处理组合器or() 和 and()or_else() 和 and_then()filtermap() 和 map_err()map_or() 和 map_or_else()ok_or() and ok_or_else() 自定义错误类型错误转换 From 特征 归一化不同的错误类型Box<dyn Error>自定义错误类型 简化错误处理thiserroranyhow 错误处理…...

python+Django 使用apscheduler实现定时任务 管理调度

apscheduler实现定时任务 管理调度 在Django 项目中经常会用到定时任务去处理一些业务处理 使用 APScheduler 可以轻松地实现定时任务的管理和调度。你可以通过以下步骤来创建、启动、停止和删除定时任务&#xff1a; 1.创建调度器对象&#xff1a; from apscheduler.schedu…...

Java编程中,异步操作流程中,最终一致性以及重试补偿的设计与实现

一、背景 微服务设计中&#xff0c;跨服务的调用&#xff0c;由于网络或程序故障等各种原因&#xff0c;经常会出现调用失败而需要重试。另外&#xff0c;在异步操作中&#xff0c;我们提供接口让外部服务回调。回调过程中&#xff0c;也可能出现故障。 这就要求我们主动向外…...

吴恩达《机器学习》8-7:多元分类

在机器学习领域&#xff0c;经常会遇到不止两个类别的分类问题。这时&#xff0c;需要使用多类分类技术。本文将深入探讨多类分类&#xff0c;并结合学习内容中的示例&#xff0c;了解神经网络在解决这类问题时的应用。 一、理解多类分类 多类分类问题是指当目标有多个类别时…...

Postman批量运行用例

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 一&#xff09;注意点 有上传文件的接口&#xff0c;需要做如下设置&#xff1a; 1、打开能读取外部文件的开关 2、把需要…...

20个Golang最佳实践

在本教程中&#xff0c;我们将探讨 Golang 中的 20 个最佳编码实践。它将帮助您编写有效的 Go 代码。 #20&#xff1a;使用正确的缩进 良好的缩进使您的代码具有可读性。一致地使用制表符或空格&#xff08;最好是制表符&#xff09;并遵循 Go 标准缩进约定。 package main …...

Java 类之 java.lang.System

Java 类之 java.lang.System 文章目录 Java 类之 java.lang.System一、简介二、主要功能1、currentTimeMillis() - 获取当前时间的毫秒数说明代码示例 2、getProperty(String key) - 获取系统属性说明代码示例 3、exit(int status) - 终止虚拟机说明代码示例 4、arraycopy(Obje…...

认识Modbus通信协议(笔记)

Modbus 莫迪康 1979年 PLC 1969年 什么是modbus&#xff1f; 它是一个Bus&#xff0c;即总线协议。比如串口协议、IIC协议、SPI都是通信协议。 协议&#xff0c;顾名思义是一种规定和约束 Modbus协议是一种引用层的报文传输协议&#xff0c;RTU、ASCLL、TCP,都属于Modbus协…...

【算法】距离(最近公共祖先节点)

题目 给出 n 个点的一棵树&#xff0c;多次询问两点之间的最短距离。 注意&#xff1a; 边是无向的。所有节点的编号是 1,2,…,n。 输入格式 第一行为两个整数 n 和 m。n 表示点数&#xff0c;m 表示询问次数&#xff1b; 下来 n−1 行&#xff0c;每行三个整数 x,y,k&am…...

基于SpringBoot的SSMP整合案例(消息一致性处理与表现层开发)

消息一致性处理 在后端执行完相应的操作后&#xff0c;我们需要将执行操作后的结果与数据返回前端&#xff0c;前端 调用我们传回去的数据&#xff0c;前端是如何知道我们传回去的数据名称的&#xff1f; 答&#xff1a;前后端遵循了同一个"协议"。这个协议就是定义…...

c#之反射详解

总目录 文章目录 总目录一、反射是什么&#xff1f;1、C#编译运行过程2、反射与元数据3、反射的优缺点 二、反射的使用1、反射相关的类和命名空间1、System.Type类的应用2、System.Activator类的应用3、System.Reflection.Assembly类的应用4、System.Reflection.Module类的应用…...

synchronized jvm实现思考

底层实现时&#xff0c;为什么使用了cxq队列和entryList双向链表&#xff1f;这里为什么不跟AQS中使用一个队列就行了&#xff0c;加了一个entryList的目的是为了什么&#xff1f; 个人理解这里多一个entryList&#xff0c;可能是用于减少频繁的cas操作。假设存在很多锁竞争时&…...