如何将一个8s的接口优化到500ms以下
最近换了个工作,刚入职就接了个活--优化公司自营app的接口性能,提升用户体验。
刚开始还以为是1s优化到500ms这种,或者500ms优化到200ms的接口,感觉还挺有挑战的。下好app体验了一下。好家伙,那个慢已经超过了我的忍耐力,瞬间就不想用这个app了,挺佩服那些坚持使用该app的用户。
生产环境其实还好,因为服务器配置好,RT保持在3~4s左右,测试环境就不行了,8s起步。测试同学每次为了测一个下单的小功能,就要先进行搜索,详情,订单确认等接口,累计都快30s的前置操作。我更好奇测试同学这几年是怎么坚持下来的。
还要说下公司的项目,微信小程序起步,最开始是找了个外包团队,用了一套很古老的代码完成的,只能保证功能正常进行,但是代码性能就从没考虑过(想必接过外包活的朋友应该深有体会)。去年开始研发了app,重心逐步向app靠拢了。但是过慢的响应严重影响了公司的推广,于是就把这个活交给了我。
出于篇幅和隐私原因,本文不太会涉及具体业务代码和SQL,部分SQL也会换成通用的表来说明,只是会说一下整个优化的思路。
好了,闲话不多说,马上进入正题。
宏观优化
第一步:sql优化
在做性能优化的第一步,一定是先看有没有慢查,因为这个是不需要做任何分析就能拿到的结果。先找公司运维同事拉了一份慢查清单,真的是让我大开眼界。
10张表的关联查询,无处不在的or语句,like '%%'语句等等,甚至有些sql由于是mybatis-plus生成的sql语句,都找不到从哪调用的。没办法,一步一步来吧。
首先是表关联查询,10张表的sql我实在优化不动,看了下对应的接口QPS,不太高,最先放弃了,这个只能是等以后系统重构再考虑了。
or语句也改的简单粗暴,直接用union all改造,相同的sql结果,RT直降90%。改法如下:
--原sql
select * from user_info where user_id ='' or user_name like '%%' ;--新sql
select * from user_info where user_id ='' union all select * from user_info where user_name like '%%' ;
原理说明:or语句会破坏索引,改为union all以后,每条sql都会走各自的索引(先忽略那个like语句不走索引的问题),这样会大幅提升sql性能
like语句确实在当前技术层面下无法解决,要么改es,要么改需求(明确前缀匹配,或者强制带一个命中索引的条件)。
还要说一个点,也是我们经常容易犯的问题,就是索引缺失。在最开始的需求时,只用user_id这个字段查询,所以对它加了索引,过了一两年以后,需求变更,现在要对user_name也要进行查询了,写代码的时候就会忘记给user_name增加索引。又或者在数据量很低的情况下,没有命中索引也不慢,察觉不到,过了一两年数据累积多了,没有索引的问题就显露了。这种情况很好发现也很好解决,拿着慢查sql执行一下explain,就能发现问题,增加索引即可。
当然有时加索引并不是100%能解决问题,有可能提交的索引会被DBA打回,这就需要结合业务进行判断了,具体不赘述。
最后呢,我再补充个索引失效的案例。
select * from user_info where user_id in (select user_id from order where create_time>='')
这条sql非常慢,explain显示user_info表没有走索引,但是user_id是有索引的。进一步探究发现,子查询的结果最多也就10条数据,当我拿着查到的10条数据改为如下sql时,又显示走了user_id索引,非常神奇
select * from user_info where user_id in (1,2,3,4,5,6,7,8,9,10)
查询了一些资料得知,当in语句明确数量时,sql解析器是可以走索引的(除非in的数量过大);但是当in是子查询的时候,解析器在选择索引时,由于不太清楚in的数量,并且user_info表的数量也不算太多(大概100万左右吧),它会选择主键索引去关联,这样就导致了user_id索引失效。
知道原因了就很好改了,稍微改一下代码,改成exist语句或者把子查询改成两段分别查询的代码即可。
第二步:串行调用改为并行调用
以详情页为例,这个接口调用了接近20个服务(RPC或查DB)来组装详情页信息,每一个查询又都是线性执行的,假设平均一个接口在100毫秒左右,光这些接口调用就用了2s+的时间,详情页不慢才怪。
这里的改动目标就很明确了,梳理一下这20个服务的层级关系,然后改为并行调用。
-
把没有相互数据依赖的接口放在一个CompletableFuture中并行调用。
-
有相互依赖的,可以用thenAcceptAsync调用,也可以放在另一个CompletableFuture中并行调用。这个选择方式,我会在第六步中详细说明一下。
这一步没什么可细说的,按照标准的并行开发方式改造即可,做好并发编程的一些基本防护,比如用独立的线程池,做好线程超时,线程报错等措施。
宏观层面的改造基本就算完成了。为什么要说是“宏观”呢,因为这两步改造不仅在慢的问题发现上很容易,在改造完成后的体验上也有质的飞跃。我在做完这两步以后,生产RT降到了1s左右,测试RT降到了2s左右。
微观优化
宏观层面的改造,发现容易,改造快,见效明显。如果我们的代码都是按规范实现的,那基本上宏观的改造完成后,接口的优化也就差不多了。如果还要提高接口性能,就要进行非常细致的接口梳理、分析,在拿到整个接口流程后,才能制定更详细的优化方案,这一步我称之为微观优化。
第三步:重复调用
重复调用是一个常见问题,通常发生在一个经过多年,多人,多需求迭代后的接口中。一般来说,重复调用分为两种类型,三个场景:
-
同一个接口反复调用
-
一个接口中反复调用。通常来说,比较大众化的接口,如用户信息,商品信息等,后来的开发者会有意识的去查看整个接口的前置步骤有没有调用过,有的话就直接复用出参了。但如果是一个非常小众的接口,仅在特定的场景下调用,开发者可能就不会很仔细的阅读前置代码,直接在自己的子方法中调用了,而实际上该接口很有可能会在上面某个子方法中被调用过了。
-
上下游接口中反复调用。这个是微服务中经常遇到的,尤其是大众化接口,比如查商品信息,有可能整个链路中,80%的服务都会重新查询一次商品服务。这个的优化需要考虑的点非常多,由于不属于我们今天所讨论的范畴,所以本文就忽略该场景了。
-
-
同一个下游服务类似接口反复调用。比如当前查询了用户接口,获取用户基本信息;过一段时间,需要获取用户积分,你期望在原接口上增加积分出参,但用户系统的同事会说,那个接口调用量太大,不建议增加出参增大RT,推荐给你一个专门查积分的接口;又过了一段时间,类似的情况查询用户会员等级。。。这样不断的累积下,导致你的服务RT越来越高。
我在本次的梳理中,这两个类型都遇到了,并且公司项目并没有分团队,所有代码都可以改。针对类型1,在梳理完流程后,把重复调用的方法从子方法抽到主方法中,作为参数传递;针对类型2,针对当前业务,重新写一个新接口,一次性把用户基本信息,积分,等级等信息拿到。
如果下游服务(如上文所述用户)并不归属你部分所管辖,也可以说服对应的同事帮你提供一个新的接口,比如我在前司就写过很多getxxxForOrder,getxxxForProduct等专用接口,因为这类合并查询的接口,可以显著降低下游服务的QPS,他们也会有动力去优化的。
第四步:循环中查db/远程服务
这是编程中的一个大忌,如果下游不愿意提供批量接口,或者是异步操作,又或者循环数量在可预见的未来,也不会超过2~3个,这么搞是可以的。但如果数量很大的情况下,这么写代码就不推荐了。
我在改造中遇到的非常夸张的点,是查询一个列表页接口,先根据主表分页查询到数据后,循环获取页面上需要的其他数据,比如订单表可能只有订单号,商品id,用户id等,拿到以后,循环调用商品接口,用户接口获取对应的商品名称、用户信息等。
这里的改造就是先拿到当前页需要展示商品id和用户id,调用批量接口拿到所有的商品信息和用户信息,再将这些数据转成map(重点),在循环中用map.get(userId)或者map.getOrDefault(userId,"xxx")的方式拿到对应数据。
//注意,最后的(v1,v2)->v1一定要加上,即使你确定不会出现重复数据,养成这个习惯还是非常好的
Map<Long, String> map = productList.stream().collect(Collectors.toMap(
o -> o.getProductId(), ProductInfo::getProductName,(v1,v2)->v1));
查db的也类似,不过有一点会让很多人困惑,感觉只能循环中查询,即:一对多或者多对多的表中,每次的查询条件都不一样。其实这种情况下,只需要将所有的数据全部in查询即可,然后转map的时候,将key进行拼接。
如下所述:
--比如一个活动(a)对应多个商品(p),一个商品也可能对应多个活动
--a1->p1,p2;a2->p3,p4;
--p1->a1,a3,p2->a1,a4;
--通常情况,循环中挨个查询的sql为:
select * from activity_product_rel where product_id='p1' and activity_id in('a1','a3'); select * from activity_product_rel where product_id='p2' and activity_id in('a1','a4');
--不妨改为如下SQL
select * from activity_product_rel where product_id in('p1','p2','p3') and activity_id in('a1','a2','a3','a4');Map<String, Object> map = activityProductList.stream().collect(Collectors.toMap
(o -> o.getActivityId() +"_"+ o.getProduct() , Function.identity(),(v1,v2)->v1));
//或者是下面的方式,根据实际情况选择
Map<String, List<Object>> map =activityProductList.stream().collect(
Collectors.groupingBy(o -> o.getActivityId() +"_"+ o.getProduct() ));
//循环中用如下方式获取数据
map.get(activityId+"_"+productId)
要说的一点是,sql的提前查询也要考虑实际情况,不能滥用,如果真IN出来成百上千的数据,那还不如循环查询呢,总之,要结合db性能,索引命中情况后再做决定。
第五步:拆分in语句
这一步既是上一步的补充,也可以看成是独立的一点。
在正常的sql查询中,也会出现in语句中存在上千甚至上万的数据,严重影响了db性能,甚至出现不走索引的情况,这时就需要拆分in语句了,把一条sql拆成10条,并行执行,然后聚合查询结果。
自己简单实现了下面这个方法。
public static <T> List<T> splitIn(BaseMapper<T> mapper, QueryWrapper<T> wrapper, List<Object> inData, String column, ThreadPoolExecutor pool, int size) {if (inData.size() <= size*1.1) {QueryWrapper<T> newWrapper = wrapper.clone();newWrapper.in(column, inData);return mapper.selectList(newWrapper);}List<List<Object>> partition =ThreadPoolUtils.balancedPartition(inData,size);Function<List<Object>, List<T>> function = data -> {QueryWrapper<T> newWrapper = wrapper.clone();newWrapper.in(column, data);return mapper.selectList(newWrapper);};return AsyncUtil.supplyAsync(partition, function, pool).stream().collect(ArrayList::new, List::addAll, List::addAll);
}
public static <P, R> List<R> supplyAsync(List<P> paramList, Function<P, R> apply, ThreadPoolExecutor executor) {if (CollectionUtils.isEmpty(paramList)) {return new ArrayList<>(0);}List<R> result = new ArrayList<>();List<CompletableFuture<R>> futures = new ArrayList<>();for (P p : paramList) {CompletableFuture<R> future = CompletableFuture.supplyAsync(() -> apply.apply(p), executor);futures.add(future);}for (CompletableFuture<R> future : futures) {R r = future.join();result.add(r);}return result;
}
public static <T> List<List<T>> balancedPartition(List<T> list, int partitionCount) {int size = list.size();int baseSize = size / partitionCount;int remainder = size % partitionCount;List<List<T>> result = Lists.newArrayList();int start = 0;for (int i = 0; i < partitionCount; i++) {int end = start + baseSize + (i < remainder ? 1 : 0);result.add(list.subList(start, Math.min(end, size)));start = end;}return result;
}
主要说明三点:
-
inData.size() <= size*1.1这个判断主要是基于一些实际情况,比如size是1000,如果是1001的时候,明明和1000没啥区别,却被迫分组,这种情况下多线程反而比单线程要慢,所以增加了一定的冗余。这个可以根据实际情况做调整。
-
balancedPartition这个方法是让deepseek写的,自己也懒得动脑筋想算法了。原本用的是Lists.partition()做分组,但它的实际效果是,1001个数据按照size=1000分组,会变成一组1000个,一组1个,极度不平均,也会影响异步的总RT,于是就想找个平均拆分的算法。
-
线程池和数量都作为入参传入,所以让不同的场景各自定义不同的数据,甚至可以通过配置中心来实时调整。
第六步:CompletableFuture线程分组
调试过程中,我把第二步优化的每一个方法的RT都打印,把异步的总时间也都打印,发现最慢的一个接口500ms左右,但是异步总时长却在1s左右。
在理想情况下,异步的总时长应该只会比最慢的接口再慢二三十毫秒,现在既然慢了快一倍,那就说明存在线程等待问题。
而解决线程等待的问题上,不是一味的加大线程池数量就可以的,这样治标不治本的,反而会加重CPU的负担。在仔细查看了所有接口的RT后,我决定做线程池分组。
目前分成了heavy和lite两个组,把那种大于300ms的服务放入heavy中,其他的放入lite中。这样可以极大实现线程的复用,这么做了一个简单的分组后,就实现了理想的效果:最慢接口500+ms,异步总时长在550ms左右。
基于此,我们再聊下上面留得一个点,有接口依赖的异步,是用thenAcceptAsync还是放在另一个CompletableFuture中并行调用。
我的理解是,根据实际情况做分析,简单说下我的想法:
-
如果上游接口耗时较短,那么下游接口可以放在thenAcceptAsync调用,毕竟即使这样做了,总时长可能都超不过最慢的接口
-
如果有非常多的接口依赖同一个接口,那不如将它们和其他接口一并放到第二个CompletableFuture中执行。
其实说白了,就是尽最大可能压榨CPU性能,降低RT,哪个方式快用哪个。
最后要说一个必须注意的点,如果没有必要,不要在异步中使用异步,如果一定要使用,那线程池一定要分开,不然会出现主子线程死锁情况。
第七步:并行计算
上面的很多异步情况,其实都是指的IO密集型,如果是CPU密集型的情况下,我们通常是不太会考虑用并行的。但在本次优化中,我发现了这么一个点。
有一个计算金额的方法,速度很快,1~3ms内就能结束,所以for循环去处理也没啥大问题,但调用这个方法的总时长超过了300ms,仔细分析后发现,这里有200多个数据需要计算,虽然每一个都很快,但累积下来也是一个不小的数字了,我将foreach改为list.parallelStream().forEach后,RT骤降到30ms左右。
但还没有结束,list.parallelStream().forEach仅建议用来验证并行能否提高性能,并不建议实际开发中使用,因为它的底层使用的时候ForkJoin框架来拆分任务的,在某些情况下可能会导致主线程阻塞。我在前司就被这个点坑过,为两年前的代码买单。。。
所以如果想要使用并行计算,一定要自定义线程池,不要想着去赌不出问题的概率。
第八步:不要滥用redis
说起来,这个外包项目代码是极度迷信redis,几乎所有的地方都用到了redis,导致大key非常多,调用一次redis的时间比查mysql要慢多了,并且还很容易引发YGC甚至FULLGC。
所以我也对项目中redis的使用进行了一定程度的降温。具体体现在如下几点:
-
针对分页数据,redis可以存全量数据,但仅存核心数据,其余数据都需要通过查表来补充。其实这一步应该用es甚至纯查表来实现,奈何改动量太大,公司还没有es环境,退而求其次了。
-
for循环调用redis的地方,有些都改成了查一次db。这也是经过性能比对后的结果,主要还是redis每次查的数据量太大了。
写到这里,基本上通用的优化方案就说完了。这么一通改造下来,测试环境的RT已经完美的降到1s左右了,想想当初的8s,简直不敢想象。
个性化点
或者说“非通用性优化”。这些优化点可能只适用于我司,因为大部分改动都属于历史代码堆叠导致,改动上没什么借鉴意义,不过可以参考下梳理过程。
第九步:接口拆分
这也算是一个通病,就是如果一个底层服务的部分出参,能满足一个新接口的全部功能,那就直接复用,省时省力。但很多时候,这个底层服务有可能是给一个聚合服务提供的,内部有大量对于新接口无用的功能。
就比如一个查询商品图片的接口,入参productId,出参imgList,controller直接调用了商品详情页接口,把什么商品说明,优惠券,价格详情全查了一遍,但最后只用了其中的imgList。。。
这种改造也很简单,重写一个新的service方法,只调用查商品的下游接口就好(因为懒得针对这种情况专门再写一个只查图片的接口了),结果就是这个查图片的接口RT从500+ms直降到50ms以下
第十步:跳过验签
这是最有意思的一个点,验证了好久才找到原因。
起因是通过日志发现接口仅用了800ms,但是app端的响应就是要1.5s以上。刚开始以为是出参过大(50kb以上)导致下载慢,但是抓包发现它的Waiting(TTFB)就是用时1.3s,下载20多ms。然后还专门去查了TTFB怎么回事,怎么优化之类的,就不表述了。
后来无意中发现,直接调用对应服务器的接口,即:http://192.168.1.101:8080/getXXX,就是800多ms,但是如果通过完整的url调用,即http://test.domain.com/xxxx/xxxx/getXXX这种方式,就是1.5s左右。
然后就跟到了网关代码中,发现原来是鉴权服务非常慢。。。
但是列表页,详情页这些接口,本身就支持非登录状态下查询的,没必要进行鉴权。和产品沟通认可后,对着网关一通改造,做好配置,让详情页接口跳过鉴权,接口瞬间降到1s以下,涉及出参少的详情,甚至200ms就返回了。
//网上找的查看curl执行时长的方法
//使用方法就是,如果原本的curl是curl "http://127.0.0.1",那么就在curl后面先加上下面的内容,再写"http://"
curl -w "Time to Connect: %{time_connect}\nTime to Start Transfer: %{time_starttransfer}\nTotal Time: %{time_total}\n" "http://127.0.0.1:8080/xxxx"//如果不想要看到出参,就用下面这个方法
curl -o /dev/null -w "Time to Connect: %{time_connect}\nTime to Start Transfer: %{time_starttransfer}\nTotal Time: %{time_total}\n" "http://127.0.0.1:8080/xxxx"
第十一步:跳过业务逻辑
就是某些业务逻辑其实是由于业务框架限制导致(原始代码是一套很老的系统),很多代码在公司现业务逻辑中已经没意义了,就用更直接的方式替代。有如下几个点:
-
某个RPC耗时200ms,但十多个出参中,仅用了其中一个值,且该值是一个数据库配置,我就直接改成配置中心配置了。(说明,我是经过确认,该数据库配置已经无意义了,理论上就是一个常量,为了一些考量才用配置中心代替)
-
某个查询会依据上一个查询的结果中某个字段判断,为0的时候才会查,分析后发现,数据库里就没有非0的数据,于是把把前置查询逻辑移除。(说明,经过确认,原始代码是一套sass系统,而现业务非sass,就是固定一套逻辑,这样才删除的前置查询)
-
由于app是后起之秀,很多小程序业务逻辑其实并没有包含,但是底层却是相同的代码。我根据app的出参进行对照后,重写了个app专用接口,删减了大量逻辑,直接让app接口快到飞起,降到100ms左右。(说明,这里就有点定制化的意思了,其实是和业务开发有一些背道而驰,万一以后来个需求,得两个接口一起改了。不过也是咨询过业务和产品,确定app未来的发展方向后,才这么干的)
其他点
这里想说的几个点,就属于极致优化了,对于性能不敏感的接口,是可以忽略这些点的。并且也并不全是本次优化过程中遇到的,也算是总结下自己曾经遇到的一些点吧。
beanCopy
接口中用了大量的BeanUtils.copyProperties,经过验证,总共耗时在10~15ms之间,所以对于性能敏感的接口,还是不建议使用这种方式,推荐用MapStruct进行对象的copy。
由于这个改动量有点大,并且提升有限,我就没有改。不过后续的新逻辑我都是用MapStruct进行处理的。
stream
在不使用parallelStream的前提下,stream理论上是要比普通的for循环慢的,但是太较真的话也没多必要。所以一般来说,我的使用策略是,性能不敏感接口,哪个顺手写哪个;不过如果要对同一个list的多个值做多次聚合的话,我一般都会改成for循环进行分别取值。
但说个例外吧,我确实遇到过对同一个list多维度聚合的,如果真用for去写,就太复杂了,还得考虑空map去new的情况,所以正好接口QPS不高,就用了多次groupingBy。
当然,现在有了更好的选择,将多个stream扔给deepseek让它优化,一般都能给到一个非常好的优化结果。
多次for循环
这个一般存在于历经多次迭代的代码,比如对同一个list先做了一次遍历,对userName进行脱敏处理,后面(可能一年后)又来一次遍历,对新增的价格字段做除以100。这种在没有相互参数依赖的情况下,是可以合并的。
同样,交给ai去优化,省时省力。
@Value取值的List或Set
通常会有一些业务场景,比如灰度,简易的业务配置数据等会通过配置中心来控制。我见过很多代码都是用List接收的,然后在业务代码中,用value.contains(xxx)来进行判断。
要知道list的contains效率是O(n),而set的contains效率是O(1),虽说在小数据量的情况下,他俩差距微乎其微,但毕竟二者在@Value的配置上没什么差异,养成一个好习惯还是不错的。
聊点有意思的
这里说几个我在优化过程中犯的错吧,都是比较低级的。
in语句拆分
这个就是上面第五步所说的,最开始发现in语句有340多个值,怕影响性能就按照100一个进行拆分,最后耗时300+ms,以为是分组不均导致,就改成均分模式,结果依然差不多,后来就决定试一下不分的情况,结果只有几十ms。。
这个点给我的感触就是,过度优化不一定是好优化,还是要根据实际情况进行取舍。
线程死循环
由于整个优化过程耗时3天,到自测的时候,只记得自己大致改了哪些方向,细节的改造早就忘了。在改造完成自测的时候发现,时间范围只有1天时正常,大于1天程序就无响应了,随后抛出超时异常。
因为有历史经验,第一反应是数据量大了以后,线程增多,主子线程池死锁了,于是开始排查线程池,还把好多子线程中的异步都改成了同步进行验证,结果没任何问题。
排查了好久终于定位到一个非常底层的计算方法内,会对时间做循环计算。这里按照上面所说的第四步改造的时候,整出个小bug导致死循环了。最后定位到以后真的是被自己蠢哭了。
不过也就是这次排查才发现上面in语句拆分反而很浪费时间,算是歪打正着吧。
结束
整个优化过程就说完了,最终效果就是8s的接口被优化到了500ms左右,app甚至小于150ms。只能说,能被大幅优化的代码,只能说明之前写的太烂了。
但这话调侃调侃还好,不能认为接手的代码烂,就去喷前任不懂得性能优化。毕竟产生该问题的原因非常多,可能是框架导致,可能是日常迭代导致,可能是数据少没在意,也可能没有预料到业务功能大改。随着时间的推移,没准自己就成了那个被喷的前任了不是?
我们应当保持一个平常心去对待性能优化,并对核心接口做好日常监控,发现RT有明显增幅时去主动了解原因,也许当时没时间改,可以趁着某个需求迭代时顺便改掉。
最后的最后,我想说的是,千万不要忘了,面向开关编程。
相关文章:
如何将一个8s的接口优化到500ms以下
最近换了个工作,刚入职就接了个活--优化公司自营app的接口性能,提升用户体验。 刚开始还以为是1s优化到500ms这种,或者500ms优化到200ms的接口,感觉还挺有挑战的。下好app体验了一下。好家伙,那个慢已经超过了我的忍耐…...
如何保证本地缓存和redis的一致性
1. Cache Aside Pattern(旁路缓存模式) 核心思想:应用代码直接管理缓存与数据的同步,分为读写两个流程: 读取数据: 先查本地缓存(如 Guava Cache)。若本地未命中&…...
30天学Java第十天——反射机制
反射机制 反射机制是 Java 语言中的一个重要特性,它允许程序在运行时动态地获取类的信息(如类的属性、方法和构造器等),并且可以操作这些信息。 反射机制在某些情况下非常有用,例如开发框架、库,或者需要进…...
Nodejs Express框架
参考:Node.js Express 框架 | 菜鸟教程 第一个 Express 框架实例 接下来我们使用 Express 框架来输出 "Hello World"。 以下实例中我们引入了 express 模块,并在客户端发起请求后,响应 "Hello World" 字符串。 创建 e…...
视频设备轨迹回放平台EasyCVR打造货运汽车安全互联网视频监控与管理方案
一、背景介绍 随着互联网发展,货运中介平台大量涌现,行业纠纷也随之增多。尽管当前平台APP具备录音和定位功能,但货物交易流程的全方位监控仍无法实现。主流跟踪定位服务大部分聚焦货物轨迹与车辆定位,尚未实现货物全程可视化监控…...
玩转Docker | 使用Docker部署Docmost文档管理系统
玩转Docker | 使用Docker部署Docmost文档管理系统 前言一、Docmost介绍Docmost 简介Docmost 特点二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署Docmost服务下载镜像编辑部署文件创建容器检查容器状态检查服务端口安全设置四、访问Docmost服务访问Docmos…...
docker方式项目部署(安装容器组件+配置文件导入Nacos+dockerCompose文件创建管理多个容器+私有镜像仓库Harbor)
基于docker的部署 服务器主机ip 192.168.6.131 安装组件 安装redis docker pull redis:7.0.10#在宿主机上/var/lib/docker/volumes/redis-config/_data/目录下创建一个redis配置文件 vim redis.conf#内容如下 appendonly yes #开启持久化 port 6379 #requirepass 1234 #密码…...
基于OpenCV与PyTorch的智能相册分类器全栈实现教程
引言:为什么需要智能相册分类器? 在数字影像爆炸的时代,每个人的相册都存储着数千张未整理的照片。手动分类不仅耗时,还容易遗漏重要瞬间。本文将手把手教你构建一个基于深度学习的智能相册分类系统,实现:…...
C++中string库常用函数超详细解析与深度实践
目录 一、引言 二、基础准备:头文件与命名空间 三、string对象的创建与初始化(基础) 3.1 直接初始化 3.2 动态初始化(空字符串) 3.3 基于字符数组初始化 3.4 重复字符初始化 四、核心函数详解 4.1 字符串长度相关 4.1.1 …...
数据结构(3)
实验步骤: 任务:要求使用自定义函数来实现 输入一段文本,统计每个字符出现的次数,按照字符出现次数从多到少,依次输出,格式如下: 字符1-个数 字符2-个数 ...... 解题思路: 构建结构体…...
【C++教程】使用printf语句实现进制转换
在C语言中,printf 函数可以直接实现部分进制转换功能,通过格式说明符(format specifier)快速输出不同进制的数值。以下是详细使用方法及示例代码: 一、printf 原生支持的进制转换 1. 十进制、八进制、十六进制转换 #…...
el-dialog设置append-to不生效;el-dialog设置挂载层级
文章目录 一、场景二、注意点1. append-to-body何时为true2.设置层级,遮罩层大小不生效3.相关代码 三、ElMessageBox遮罩层 效果: 一、场景 正常情况下,el-dialog的弹框是挂载在body下的,导致我们会有修改样式或者修改弹框的遮罩…...
互联网软件开发自动化平台 的多维度对比分析,涵盖架构、功能、适用场景、成本等关键指标
以下是关于 互联网软件开发自动化平台 的详细解析,涵盖其核心概念、主流平台的功能、架构设计、适用场景及对比分析: 一、自动化平台的定义与核心目标 自动化平台(如CI/CD平台)是用于 持续集成(CI) 和 持续…...
UE5 制作方块边缘渐变边框效果
该效果基于之前做的(https://blog.csdn.net/grayrail/article/details/144546427)进行修改得到,思路也很简单: 1.打开实时预览 1.为了制作时每个细节调整方便,勾选Live Update中的三个选项,开启实时预览。…...
深入探究 GRU 模型:梯度爆炸问题剖析
在深度学习领域,循环神经网络(RNN)及其变体在处理序列数据时展现出了强大的威力。其中,门控循环单元(GRU)作为 RNN 的一种进阶架构,备受关注。今天,咱们就来深入聊聊 GRU 模型&#…...
生成对抗网络(GAN)原理详解
生成对抗网络(GAN)原理详解 1. 背景 生成对抗网络(Generative Adversarial Network, GAN)由 Ian Goodfellow 等人于 2014 年提出,是一种通过对抗训练生成高质量数据的框架。其核心思想是让两个神经网络(生…...
CFD中的动量方程非守恒形式详解
在计算流体力学(CFD)中,动量方程可以写成守恒形式和非守恒形式,两者在数学上等价,但推导方式和应用场景不同。以下是对非守恒形式的详细解释: 1. 动量方程的守恒形式 首先回顾守恒形式的动量方程ÿ…...
AIoT 智变浪潮演讲实录 | 刘浩然:让硬件会思考:边缘大模型网关助力硬件智能革新
4 月 2 日,由火山引擎与英特尔联合主办的 AIoT “智变浪潮”技术沙龙在深圳成功举行,活动聚焦 AI 硬件产业的技术落地与生态协同,吸引了芯片厂商、技术方案商、品牌方及投资机构代表等 700 多位嘉宾参会。 会上,火山引擎边缘智能高…...
4.B-树
一、常见的查找方式 顺序查找 O(N) 二分查找 O(logN)(要求有序和随机访问) 二叉搜索树 O(N) 平衡二叉搜索树(AVL树和红黑树) O(logN) 哈希 O(1) 考虑效率和要求而言,正常选用 平衡二叉搜索树 和 哈希 作为查找方式。 但这两种结构适合用于数据量相对不是很大,能够一次性…...
怎么看英文论文 pdf沉浸式翻译
https://arxiv.org/pdf/2105.09492 Immersive Translate Xournal打开...
计算机三级第一章:信息安全保障概述(以时间节点推进的总结)
淡蓝色为必背内容 第一阶段:电讯技术的发明19世纪30年代:电报电话的发明 1835年:莫尔斯(Morse)发明了电报 1837年:莫尔斯电磁式有线电报问世 1878年:人工电话交换局出现 1886年:马可尼发明了无线电报机 1876年:贝尔(Bell)发明了电话机 1892年,史瑞桥自动交换…...
车载软件架构 ---单个ECU的AUTOSAR开发流程
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 周末洗了一个澡,换了一身衣服,出了门却不知道去哪儿,不知道去找谁,漫无目的走着,大概这就是成年人最深的孤独吧! 旧人不知我近况,新人不知我过…...
【场景应用7】在TPU上使用Flax/JAX对Transformers模型进行语言模型预训练
在本笔记本中,我们将展示如何使用Flax在TPU上预训练一个🤗 Transformers模型。 这里将使用GPT2的因果语言建模目标进行预训练。 正如在这个基准测试中所看到的,使用Flax/JAX在GPU/TPU上的训练通常比使用PyTorch在GPU/TPU上的训练要快得多,而且也可以显著降低成本。 Fla…...
C++运算符重载全面总结
C运算符重载全面总结 运算符重载是C中一项强大的特性,它允许程序员为自定义类型定义运算符的行为。以下是关于C运算符重载的详细总结: 一、基本概念 1. 什么是运算符重载 运算符重载是指为自定义类型(类或结构体)重新定义或重…...
PTA | 实验室使用排期
目录 题目: 输入格式: 输出格式: 输入样例: 输出样例: 样例解释: 代码: 无注释版: 有注释版: 题目: 受新冠疫情影响,当前大家的活动都…...
3.7 字符串基础
字符串 (str):和列表用法基本一致 1.字符串的创建 -str转换(字符串,可用于将其他字符类型转换为字符串) -单引号 双引号 三引号 2.索引 3.字符串的切片 4.字符串的遍历 5.字符串的格式化 6.字符串的运算符 7.字符串的函数 #…...
《 C++ 点滴漫谈: 三十三 》当函数成为参数:解密 C++ 回调函数的全部姿势
一、前言 在现代软件开发中,“解耦” 与 “可扩展性” 已成为衡量一个系统架构优劣的重要标准。而在众多实现解耦机制的技术手段中,“回调函数” 无疑是一种高效且广泛使用的模式。你是否曾经在编写排序算法时,希望允许用户自定义排序规则&a…...
16bit转8bit的常见方法(图像归一化)
文章目录 16-bit转8-bit的常用方法一、数据类型转换:image.astype(np.uint8) —— 若数值 x 超出 0-255 范围,则取模运算。如:x 600 % 256 88二、截断函数:np.clip().astype(np.uint8) —— 若数值 x 超出 0-255 范围࿰…...
消息中间件kafka,rabbitMQ
在分布式系统中,消息中间件是实现不同组件之间异步通信的关键技术。Kafka 和 RabbitMQ 是两个非常流行的消息中间件系统,它们各自有着不同的特点和应用场景。下面将分别介绍 Kafka 和 RabbitMQ,并讨论它们在消息队列中的使用。 一、Kafka (Apache Kafka) 主要特点: 高吞吐…...
C语言编译预处理3
条件编译:是对源程序的一部分指定编译条件,满足条件进行编译否则不编译。 形式1 #indef 标识符 程序段1 #else 程序段2 #endif 标识符已经被定义用#ifdef #include <stdio.h>// 可以通过注释或取消注释下面这行来控制是否定义 DEBUG 宏 // …...
数据结构·树
树的特点 最小连通图 无环 有且只有 n − 1 n-1 n−1 条边 树的建立方式 顺序存储 只适用于满n叉树,完全n叉树 1<<n 表示结点 2 n 2^n 2nP4715 【深基16.例1】淘汰赛 void solve() {cin >> n;for (int i 0; i<(1<<n); i) {cin >&g…...
队列的各种操作实现(数据结构C语言多文件编写)
1.先创建queue.h声明文件(Linux命令:touch queue.h)。编写函数声明如下(打开文件 Linux 操作命令:vim queue.h): //头文件 #ifndef __QUEUE_H__ #define __QUEUE_H__ //队列 typedef struct queue{int* arr;int in;int out;int cap;int size; }queue_t;…...
48V/2kW储能电源纯正弦波逆变器详细设计方案-可量产
48V/2kW储能电源纯正弦波逆变器详细设计方案 1.后级驱动电路图 2.前级驱动电路图 3.功率表电路原理图 4.功率板BOM: 5.后级驱动BOM 6.前级驱动BOM...
[redis进阶二]分布式系统之主从复制结构(2)
目录 一 redis的拓扑结构 (1)什么是拓扑 (2)⼀主⼀从结构 (3)⼀主多从结构 (4)树形主从结构 (5)三种拓扑结构的优缺点,以及适用场景 二 redis的复制原理 (1)复制过程 (2)数据同步psync replicationid/replid (复制id)(标注同步的数据来自哪里:数据来源) offset (偏移…...
Playwright多语言生态:跨Python_Java_.NET的统一采集方案
一、问题背景:爬虫多语言割裂的旧时代 在大规模数据采集中,尤其是学术数据库如 Scopus,开发者常遇到两个经典问题: 技术语言割裂:Python开发人员使用Selenium、requests-html等库;Java阵营使用Jsoup或Htm…...
day30 第八章 贪心算法 part04
452. 用最少数量的箭引爆气球 先排序,再算重叠区间 class Solution:def findMinArrowShots(self, points: List[List[int]]) -> int:if len(points)0:return 0points.sort(keylambda x:x[0])result 1for i in range(1, len(points)):if points[i][0] > point…...
java操作redis库,开箱即用
application.yml spring:application:name: demo#Redis相关配置redis:data:# 地址host: localhost# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最…...
clickhouse中的窗口函数
窗口函数 边界核心参数 窗口边界通过 ROWS、RANGE 或 GROUPS 模式定义,语法为: ROWS BETWEEN AND 基于 物理行位置 定义窗口,与排序键的实际值无关,适用于精确控制窗口行数 – 或 RANGE BETWEEN AND 基于 排序键的数值范围 定义窗口,适用于时间序列或连续数值的场景(…...
YZ系列工具之YZ02:字典的多功能应用
我给VBA下的定义:VBA是个人小型自动化处理的有效工具。利用好了,可以大大提高自己的工作效率,而且可以提高数据的准确度。我的教程一共九套一部VBA手册,教程分为初级、中级、高级三大部分。是对VBA的系统讲解,从简单的…...
金山科技在第91届中国国际医疗器械博览会CMEF 首发新品 展现智慧装备+AI
4月8日—11日,国家会展中心(上海),第91届中国国际医疗器械(春季)博览会(以下简称“CMEF 2025”)举办。金山科技在盛会上隆重推出年度新品——全高清电子内镜光学放大镜与肛肠测压系统…...
STM32 BOOT设置,bootloader,死锁使用方法
目录 BOOT0 BOOT1的配置含义 bootloader使用方法 芯片死锁解决方法开发调试过程中,由于某种原因导致内部Flash锁死,无法连接SWD以及JTAG调试,无法读到设备,可以通过修改BOOT模式重新刷写代码。修改为BOOT01,BOOT10…...
机器学习:让数据开口说话的科技魔法
在人工智能飞速发展的今天,「机器学习」已成为推动数字化转型的核心引擎。无论是手机的人脸解锁、网购平台的推荐系统,还是自动驾驶汽车的决策能力,背后都离不开机器学习的技术支撑。那么,机器学习究竟是什么?它又有哪…...
PDF解析示例代码学习
以下是结合多种技术实现的PDF解析详细示例(Python实现),涵盖文本、表格和扫描件处理场景: 一、环境准备与依赖安装 # 核心依赖库 pip install pdfplumber tabula-py pytesseract opencv-python mysql-connector-python 二、完整…...
【云平台监控】安装应用Ansible服务
安装应用Ansible服务 文章目录 安装应用Ansible服务资源列表基础环境一、安装Ansible1.1、部署Ansible1.2、配置主机清单1.2.1、方法11.2.2、方法2 二、Ansible命令应用基础2.1、ping模块2.2、command模块2.3、user模块2.4、group模块2.5、cron模块2.6、copy模块2.7、file模块2…...
项目执行中的目标管理:从战略到落地的闭环实践
——如何让目标不“跑偏”、团队不“掉队”? 引言:为什么目标管理决定项目成败? 根据PMI研究,47%的项目失败源于目标模糊或频繁变更。在复杂多变的项目环境中,目标管理不仅是制定KPI,更是构建“方向感-执行…...
如何优雅地处理 API 版本控制?
API 会不断发展,而用户的需求也会随之变化。那么,如何确保你的 API 在升级时不会影响现有用户?答案就是:API 版本控制。就像你更新了一个应用程序,引入了新功能,但旧功能仍然保留,让老用户继续愉…...
如何通过Radius认证服务器实现虚拟云桌面安全登录认证:安当ASP身份认证系统解决方案
引言:虚拟化时代的安全挑战 随着云计算和远程办公的普及,虚拟云桌面(如VMware Horizon、Citrix)已成为企业数字化办公的核心基础设施。然而,传统的用户名密码认证方式暴露了诸多安全隐患:弱密码易被暴力破…...
自然语言处理spaCy
spaCy 是一个流行的开源 自然语言处理(NLP) 库,专注于 高效、易用和工业化应用。它由 Explosion AI 开发,广泛应用于文本处理、信息提取、机器翻译等领域。 zh_core_web_sm 是 spaCy 提供的一个小型中文预训练语言模型࿰…...
大语言模型(LLMs)中的强化学习(Reinforcement Learning, RL)
第一部分:强化学习基础回顾 在深入探讨LLMs中的强化学习之前,我们先快速回顾一下强化学习的核心概念,确保基础扎实。 1. 强化学习是什么? 强化学习是一种机器学习范式,目标是让智能体(Agent)…...
数字后端实现Innovus DRC Violation之如何利用脚本批量解决G4:M7i DRC Violation
大家在跑完物理验证calibre DRC之后,会发现DRC里面存在一种G4:M7i的DRC违例,这种违例一般都是出现在memory的边界。今天教大家如何利用脚本来批量处理这一类DRC问题的解决。 首先,我们需要把calibre的DRC结果读取到innovus里面来,…...