接口性能优化的11个小技巧
接口性能优化对于从事后端开发的同学来说肯定再熟悉不过了因为它是一个跟开发语言无关的公共问题。该问题说简单也简单说复杂也复杂。有时候只需加个索引就能解决问题。有时候需要做代码重构。有时候需要增加缓存。有时候需要引入一些中间件比如mq。有时候需要需要分库分表。有时候需要拆分服务。等等。。。导致接口性能问题的原因千奇百怪不同的项目不同的接口原因可能也不一样。本文我总结了一些行之有效的优化接口性能的办法给有需要的朋友一个参考。1.索引接口性能优化大家第一个想到的可能是优化索引。没错优化索引的成本是最小的。你通过查看线上日志或者监控报告查到某个接口用到的某条sql语句耗时比较长。这时你可能会有下面这些疑问该sql语句加索引了没加的索引生效了没mysql选错索引了没1.1 没加索引sql语句中where条件的关键字段或者order by后面的排序字段忘了加索引这个问题在项目中很常见。项目刚开始的时候由于表中的数据量小加不加索引sql查询性能差别不大。后来随着业务的发展表中数据量越来越多就不得不加索引了。可以通过命令show index from order;能单独查看某张表的索引情况。也可以通过命令show create table order;查看整张表的建表语句里面同样会显示索引情况。通过ALTER TABLE命令可以添加索引ALTER TABLE order ADD INDEX idx_name (name);也可以通过CREATE INDEX命令添加索引CREATE INDEX idx_name ON order (name);不过这里有一个需要注意的地方是想通过命令修改索引是不行的。目前在mysql中如果想要修改索引只能先删除索引再重新添加新的。删除索引可以用DROP INDEX命令ALTER TABLE order DROP INDEX idx_name;用DROP INDEX命令也行DROP INDEX idx_name ON order;1.2 索引没生效通过上面的命令我们已经能够确认索引是有的但它生效了没此时你内心或许会冒出这样一个疑问。那么如何查看索引有没有生效呢答可以使用explain命令查看mysql的执行计划它会显示索引的使用情况。例如explain select * from order where code002;结果图片通过这几列可以判断索引使用情况执行计划包含列的含义如下图所示说实话sql语句没有走索引排除没有建索引之外最大的可能性是索引失效了。下面说说索引失效的常见原因如果不是上面的这些原因则需要再进一步排查一下其他原因。1.3 选错索引此外你有没有遇到过这样一种情况明明是同一条sql只有入参不同而已。有的时候走的索引a有的时候却走的索引b没错有时候mysql会选错索引。必要时可以使用force index来强制查询sql走某个索引。至于为什么mysql会选错索引后面有专门的文章介绍的这里先留点悬念。2. sql优化如果优化了索引之后也没啥效果。接下来试着优化一下sql语句因为它的改造成本相对于java代码来说也要小得多。下面给大家列举了sql优化的15个小技巧由于这些技巧在我之前的文章中已经详细介绍过了在这里我就不深入了。3. 远程调用很多时候我们需要在某个接口中调用其他服务的接口。比如有这样的业务场景在用户信息查询接口中需要返回用户名称、性别、等级、头像、积分、成长值等信息。而用户名称、性别、等级、头像在用户服务中积分在积分服务中成长值在成长值服务中。为了汇总这些数据统一返回需要另外提供一个对外接口服务。于是用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口然后汇总数据统一返回。调用过程如下图所示调用远程接口总耗时 530ms 200ms 150ms 180ms显然这种串行调用远程接口性能是非常不好的调用远程接口总的耗时为所有的远程接口耗时之和。那么如何优化远程接口性能呢3.1 并行调用上面说到既然串行调用多个远程接口性能很差为什么不改成并行呢如下图所示调用远程接口总耗时 200ms 200ms即耗时最长的那次远程接口调用在java8之前可以通过实现Callable接口获取线程返回结果。java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException { final UserInfo userInfo new UserInfo(); CompletableFuture userFuture CompletableFuture.supplyAsync(() - { getRemoteUserAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture bonusFuture CompletableFuture.supplyAsync(() - { getRemoteBonusAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture growthFuture CompletableFuture.supplyAsync(() - { getRemoteGrowthAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join(); userFuture.get(); bonusFuture.get(); growthFuture.get(); return userInfo; }温馨提醒一下这两种方式别忘了使用线程池。示例中我用到了executor表示自定义的线程池为了防止高并发场景下出现线程过多的问题。3.2 数据异构上面说到的用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口然后汇总数据统一返回。那么我们能不能把数据冗余一下把用户信息、积分和成长值的数据统一存储到一个地方比如redis存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id直接从redis中查询数据出来不就OK了如果在高并发的场景下为了提升接口性能远程接口调用大概率会被去掉而改成保存冗余数据的数据异构方案。但需要注意的是如果使用了数据异构方案就可能会出现数据一致性问题。用户信息、积分和成长值有更新的话大部分情况下会先更新到数据库然后同步到redis。但这种跨库的操作可能会导致两边数据不一致的情况产生。4. 重复调用重复调用在我们的日常工作代码中可以说随处可见但如果没有控制好会非常影响接口的性能。不信我们一起看看。4.1 循环查数据库有时候我们需要从指定的用户集合中查询出有哪些是在数据库中已经存在的。实现代码可以这样写public ListUser queryUser(ListUser searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } ListUser result Lists.newArrayList(); searchList.forEach(user - result.add(userMapper.getUserById(user.getId()))); return result; }这里如果有50个用户则需要循环50次去查询数据库。我们都知道每查询一次数据库就是一次远程调用。如果查询50次数据库就有50次远程调用这是非常耗时的操作。那么我们如何优化呢具体代码如下public ListUser queryUser(ListUser searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } ListLong ids searchList.stream().map(User::getId).collect(Collectors.toList()); return userMapper.getUserByIds(ids); }提供一个根据用户id集合批量查询用户的接口只远程调用一次就能查询出所有的数据。这里有个需要注意的地方是id集合的大小要做限制最好一次不要请求太多的数据。要根据实际情况而定建议控制每次请求的记录条数在500以内。4.2 死循环有些小伙伴看到这个标题可能会感到有点意外死循环也算代码中不是应该避免死循环吗为啥还是会产生死循环有时候死循环是我们自己写的例如下面这段代码while(true) { if(condition) { break; } System.out.println(do samething); }这里使用了while(true)的循环调用这种写法在CAS自旋锁中使用比较多。当满足condition等于true的时候则自动退出该循环。如果condition条件非常复杂一旦出现判断不正确或者少写了一些逻辑判断就可能在某些场景下出现死循环的问题。出现死循环大概率是开发人员人为的bug导致的不过这种情况很容易被测出来。还有一种隐藏的比较深的死循环是由于代码写的不太严谨导致的。如果用正常数据可能测不出问题但一旦出现异常数据就会立即出现死循环。4.3 无限递归如果想要打印某个分类的所有父分类可以用类似这样的递归方法实现public void printCategory(Category category) { if(category null || category.getParentId() null) { return; } System.out.println(父分类名称 category.getName()); Category parent categoryMapper.getCategoryById(category.getParentId()); printCategory(parent); }正常情况下这段代码是没有问题的。但如果某次有人误操作把某个分类的parentId指向了它自己这样就会出现无限递归的情况。导致接口一直不能返回数据最终会发生堆栈溢出。建议写递归方法时设定一个递归的深度比如分类最大等级有4级则深度可以设置为4。然后在递归方法中做判断如果深度大于4时则自动返回这样就能避免无限循环的情况。5. 异步处理有时候我们接口性能优化需要重新梳理一下业务逻辑看看是否有设计上不太合理的地方。比如有个用户请求接口中需要做业务操作发站内通知和记录操作日志。为了实现起来比较方便通常我们会将这些逻辑放在接口中同步执行势必会对接口性能造成一定的影响。接口内部流程图如下这个接口表面上看起来没有问题但如果你仔细梳理一下业务逻辑会发现只有业务操作才是核心逻辑其他的功能都是非核心逻辑。在这里有个原则就是核心逻辑可以同步执行同步写库。非核心逻辑可以异步执行异步写库。上面这个例子中发站内通知和用户操作日志功能对实时性要求不高即使晚点写库用户无非是晚点收到站内通知或者运营晚点看到用户操作日志对业务影响不大所以完全可以异步处理。通常异步主要有两种多线程 和 mq。5.1 线程池使用线程池改造之后接口逻辑如下发站内通知和用户操作日志功能被提交到了两个单独的线程池中。这样接口中重点关注的是业务操作把其他的逻辑交给线程异步执行这样改造之后让接口性能瞬间提升了。但使用线程池有个小问题就是如果服务器重启了或者是需要被执行的功能出现异常了无法重试会丢数据。那么这个问题该怎么办呢5.2 mq使用mq改造之后接口逻辑如下对于发站内通知和用户操作日志功能在接口中并没真正实现它只发送了mq消息到mq服务器。然后由mq消费者消费消息时才真正的执行这两个功能。这样改造之后接口性能同样提升了因为发送mq消息速度是很快的我们只需关注业务操作的代码即可。6. 避免大事务很多小伙伴在使用spring框架开发项目时为了方便喜欢使用Transactional注解提供事务功能。没错使用Transactional注解这种声明式事务的方式提供事务功能确实能少写很多代码提升开发效率。但也容易造成大事务引发其他的问题。下面用一张图看看大事务引发的问题。从图中能够看出大事务问题可能会造成接口超时对接口的性能有直接的影响。我们该如何优化大事务呢少用Transactional注解将查询(select)方法放到事务外事务中避免远程调用事务中避免一次性处理太多数据有些功能可以非事务执行有些功能可以异步处理7. 锁粒度在某些业务场景中为了防止多个线程并发修改某个共享数据造成数据异常。为了解决并发场景下多个线程同时修改数据造成数据不一致的情况。通常情况下我们会加锁。但如果锁加得不好导致锁的粒度太粗也会非常影响接口性能。7.1 synchronized在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分布式锁、zookeeper分布式锁 和 数据库分布式锁。由于zookeeper分布式锁的性能不太好真实业务场景用的不多这里先不讲。下面聊一下redis分布式锁。7.2 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; }上面代码将加锁的范围缩小了只有创建目录时才加了锁。这样看似简单的优化之后接口性能能提升很多。说不定会有意外的惊喜喔。哈哈哈。7.3 数据库分布式锁mysql数据库中主要有三种锁表锁加锁快不会出现死锁。但锁定粒度大发生锁冲突的概率最高并发度最低。行锁加锁慢会出现死锁。但锁定粒度最小发生锁冲突的概率最低并发度也最高。间隙锁开销和加锁时间界于表锁和行锁之间。它会出现死锁锁定粒度界于表锁和行锁之间并发度一般。并发度越高意味着接口性能越好。所以数据库锁的优化方向是优先使用行锁其次使用间隙锁再其次使用表锁。赶紧看看你用对了没8.分页处理有时候我会调用某个接口批量查询数据比如通过用户id批量查询出用户信息然后给这些用户送积分。但如果你一次性查询的用户数量太多了比如一次查询2000个用户的数据。参数中传入了2000个用户的id远程调用接口会发现该用户查询接口经常超时。调用代码如下ListUser users remoteCallUser(ids);众所周知调用接口从数据库获取数据是需要经过网络传输的。如果数据量太大无论是获取数据的速度还是网络传输受限于带宽都会导致耗时时间比较长。那么这种情况要如何优化呢答分页处理。将一次获取所有的数据的请求改成分多次获取每次只获取一部分用户的数据最后进行合并和汇总。其实处理这个问题要分为两种场景同步调用 和 异步调用。8.1 同步调用如果在job中需要获取2000个用户的信息它要求只要能正确获取到数据就好对获取数据的总耗时要求不太高。但对每一次远程接口调用的耗时有要求不能大于500ms不然会有邮件预警。这时我们可以同步分页调用批量查询用户信息接口。具体示例代码如下ListListLong allIds Lists.partition(ids,200); for(ListLong batchIds:allIds) { ListUser users remoteCallUser(batchIds); }代码中我用的google的guava工具中的Lists.partition方法用它来做分页简直太好用了不然要巴拉巴拉写一大堆分页的代码。8.2 异步调用如果是在某个接口中需要获取2000个用户的信息它考虑的就需要更多一些。除了需要考虑远程调用接口的耗时之外还需要考虑该接口本身的总耗时也不能超时500ms。这时候用上面的同步分页请求远程接口肯定是行不通的。那么只能使用异步调用了。代码如下ListListLong allIds Lists.partition(ids,200); final ListUser result Lists.newArrayList(); allIds.stream().forEach((batchIds) - { CompletableFuture.supplyAsync(() - { result.addAll(remoteCallUser(batchIds)); return Boolean.TRUE; }, executor); })使用CompletableFuture类多个线程异步调用远程接口最后汇总结果统一返回。9.加缓存解决接口性能问题加缓存是一个非常高效的方法。但不能为了缓存而缓存还是要看具体的业务场景。毕竟加了缓存会导致接口的复杂度增加它会带来数据不一致问题。在有些并发量比较低的场景中比如用户下单可以不用加缓存。还有些场景比如在商城首页显示商品分类的地方假设这里的分类是调用接口获取到的数据但页面暂时没有做静态化。如果查询分类树的接口没有使用缓存而直接从数据库查询数据性能会非常差。那么如何使用缓存呢9.1 redis缓存通常情况下我们使用最多的缓存可能是redis和memcached。但对于java应用来说绝大多数都是使用的redis所以接下来我们以redis为例。由于在关系型数据库比如mysql中菜单是有上下级关系的。某个四级分类是某个三级分类的子分类这个三级分类又是某个二级分类的子分类而这个二级分类又是某个一级分类的子分类。这种存储结构决定了想一次性查出这个分类树并非是一件非常容易的事情。这就需要使用程序递归查询了如果分类多的话这个递归是比较耗时的。所以如果每次都直接从数据库中查询分类树的数据是一个非常耗时的操作。这时我们可以使用缓存大部分情况接口都直接从缓存中获取数据。操作redis可以使用成熟的框架比如jedis和redisson等。用jedis伪代码如下String json jedis.get(key); if(StringUtils.isNotEmpty(json)) { CategoryTree categoryTree JsonUtil.toObject(json); return categoryTree; } return queryCategoryTreeFromDb();先从redis中根据某个key查询是否有菜单数据如果有则转换成对象直接返回。如果redis中没有查到菜单数据则再从数据库中查询菜单数据有则返回。此外我们还需要有个job每隔一段时间从数据库中查询菜单数据更新到redis当中这样以后每次都能直接从redis中获取菜单的数据而无需访问数据库了。这样改造之后能快速的提升性能。但这样做性能提升不是最佳的还有其他的方案我们一起看看下面的内容。9.2 二级缓存上面的方案是基于redis缓存的虽说redis访问速度很快。但毕竟是一个远程调用而且菜单树的数据很多在网络传输的过程中是有些耗时的。有没有办法不经过请求远程就能直接获取到数据呢答使用二级缓存即基于内存的缓存。除了自己手写的内存缓存之后目前使用比较多的内存缓存框架有guava、Ehcache、caffine等。我们在这里以caffeine为例它是spring官方推荐的。第一步引入caffeine的相关jar包dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId version2.6.0/version /dependency第二步配置CacheManager开启EnableCachingConfiguration EnableCaching public class CacheConfig { Bean public CacheManager cacheManager(){ CaffeineCacheManager cacheManager new CaffeineCacheManager(); //Caffeine配置 CaffeineObject, Object caffeine Caffeine.newBuilder() //最后一次写入后经过固定时间过期 .expireAfterWrite(10, TimeUnit.SECONDS) //缓存的最大条数 .maximumSize(1000); cacheManager.setCaffeine(caffeine); return cacheManager; } }第三步使用Cacheable注解获取数据Service public class CategoryService { Cacheable(value category, key #categoryKey) public CategoryModel getCategory(String categoryKey) { String json jedis.get(categoryKey); if(StringUtils.isNotEmpty(json)) { CategoryTree categoryTree JsonUtil.toObject(json); return categoryTree; } return queryCategoryTreeFromDb(); } }调用categoryService.getCategory()方法时先从caffine缓存中获取数据如果能够获取到数据则直接返回该数据不进入方法体。如果不能获取到数据则再从redis中查一次数据。如果查询到了则返回数据并且放入caffine中。如果还是没有查到数据则直接从数据库中获取到数据然后放到caffine缓存中。具体流程图如下该方案的性能更好但有个缺点就是如果数据更新了不能及时刷新缓存。此外如果有多台服务器节点可能存在各个节点上数据不一样的情况。由此可见二级缓存给我们带来性能提升的同时也带来了数据不一致的问题。使用二级缓存一定要结合实际的业务场景并非所有的业务场景都适用。但上面我列举的分类场景是适合使用二级缓存的。因为它属于用户不敏感数据即使出现了稍微有点数据不一致也没有关系用户有可能都没有察觉出来。10. 分库分表有时候接口性能受限的不是别的而是数据库。当系统发展到一定的阶段用户并发量大会有大量的数据库请求需要占用大量的数据库连接同时会带来磁盘IO的性能瓶颈问题。此外随着用户数量越来越多产生的数据也越来越多一张表有可能存不下。由于数据量太大sql语句查询数据时即使走了索引也会非常耗时。这时该怎么办呢答需要做分库分表。如下图所示图中将用户库拆分成了三个库每个库都包含了四张用户表。如果有用户请求过来的时候先根据用户id路由到其中一个用户库然后再定位到某张表。路由的算法挺多的根据id取模比如id7有4张表则7%43模为3路由到用户表3。给id指定一个区间范围比如id的值是0-10万则数据存在用户表0id的值是10-20万则数据存在用户表1。一致性hash算法分库分表主要有两个方向垂直和水平。说实话垂直方向即业务方向更简单。在水平方向即数据方向上分库和分表的作用其实是有区别的不能混为一谈。分库是为了解决数据库连接资源不足问题和磁盘IO的性能瓶颈问题。分表是为了解决单表数据量太大sql语句查询数据时即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。分库分表可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。如果在有些业务场景中用户并发量很大但是需要保存的数据量很少这时可以只分库不分表。如果在有些业务场景中用户并发量不大但是需要保存的数量很多这时可以只分表不分库。如果在有些业务场景中用户并发量大并且需要保存的数量也很多时可以分库分表。11. 辅助功能优化接口性能问题除了上面提到的这些常用方法之外还需要配合使用一些辅助功能因为它们真的可以帮我们提升查找问题的效率。11.1 开启慢查询日志通常情况下为了定位sql的性能瓶颈我们需要开启mysql的慢查询日志。把超过指定时间的sql语句单独记录下来方面以后分析和定位问题。开启慢查询日志需要重点关注三个参数slow_query_log 慢查询开关slow_query_log_file 慢查询日志存放的路径long_query_time 超过多少秒才会记录日志通过mysql的set命令可以设置set global slow_query_logON; set global slow_query_log_file/usr/local/mysql/data/slow.log; set global long_query_time2;设置完之后如果某条sql的执行时间超过了2秒会被自动记录到slow.log文件中。当然也可以直接修改配置文件my.cnf[mysqld] slow_query_log ON slow_query_log_file /usr/local/mysql/data/slow.log long_query_time 2但这种方式需要重启mysql服务。很多公司每天早上都会发一封慢查询日志的邮件开发人员根据这些信息优化sql。11.2 加监控为了出现sql问题时能够让我们及时发现我们需要对系统做监控。目前业界使用比较多的开源监控系统是Prometheus。它提供了 监控 和 预警 的功能。架构图如下我们可以用它监控如下信息接口响应时间调用第三方服务耗时慢查询sql耗时cpu使用情况内存使用情况磁盘使用情况数据库使用情况等等。。。它的界面大概长这样子可以看到mysql当前qps活跃线程数连接数缓存池的大小等信息。如果发现数据量连接池占用太多对接口的性能肯定会有影响。这时可能是代码中开启了连接忘了关或者并发量太大了导致的需要做进一步排查和系统优化。截图中只是它一小部分功能如果你想了解更多功能可以访问Prometheus的官网https://prometheus.io/11.3 链路跟踪有时候某个接口涉及的逻辑很多比如查数据库、查redis、远程调用接口发mq消息执行业务代码等等。该接口一次请求的链路很长如果逐一排查需要花费大量的时间这时候我们已经没法用传统的办法定位问题了。有没有办法解决这问题呢用分布式链路跟踪系统skywalking。架构图如下通过skywalking定位性能问题在skywalking中可以通过traceId全局唯一的id串联一个接口请求的完整链路。可以看到整个接口的耗时调用的远程服务的耗时访问数据库或者redis的耗时等等功能非常强大。之前没有这个功能的时候为了定位线上接口性能问题我们还需要在代码中加日志手动打印出链路中各个环节的耗时情况然后再逐一排查。如果你用过skywalking排查接口性能问题不自觉的会爱上它的。如果你想了解更多功能可以访问skywalking的官网https://skywalking.apache.org/感谢每一个认真阅读我文章的人礼尚往来总是要有的虽然不是什么很值钱的东西如果你用得到的话可以直接拿走这些资料对于【软件测试】的朋友来说应该是最全面最完整的备战仓库这个仓库也陪伴上万个测试工程师们走过最艰难的路程希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取

相关新闻

太强了!SpringBoot + QLExpress打造动态规则引擎!

太强了!SpringBoot + QLExpress打造动态规则引擎!

作为一名后端开发,你是否遇到过这样的场景: 运营同学说:"双11活动规则临时调整,满300减60改成满200减50,能马上上线吗?" 产品经理说:"风控策略需要微调一下,这个规则…

2026/7/6 8:14:59 阅读更多 →
Spring的自定义注解与处理器

Spring的自定义注解与处理器

1. 自定义注解的定义 自定义注解是 Java 中的一种元数据标记,允许在代码中添加自定义信息,并能通过反射机制进行处理。我们首先定义一个自定义注解,然后在 Spring 中使用该注解来标识需要处理的类或方法。 示例:定义一个简单的自…

2026/7/6 9:04:33 阅读更多 →
日化产品想贴牌?源头厂家直供,成本降一半

日化产品想贴牌?源头厂家直供,成本降一半

想要打造属于自己的洗发水、洗衣液、面膜或者洗手液品牌吗?贴牌生产(OEM)确实是一条最为快捷的路径。但许多刚开始涉足这个领域的新手,在第一步就遇到了麻烦:他们找不到真正的源头厂家,反而被贸易商一层又一层地加价&…

2026/7/6 9:04:07 阅读更多 →

最新新闻

Playwright网络请求拦截与模拟实战:从原理到六大应用场景详解

Playwright网络请求拦截与模拟实战:从原理到六大应用场景详解

1. 项目概述与核心价值 如果你正在用 Playwright 做自动化测试或者数据抓取,有没有遇到过这样的场景:测试一个下单流程,但支付接口总是不稳定,导致你的测试脚本动不动就失败;或者你想测试前端页面在网络超时或服务器返…

2026/7/6 9:04:10 阅读更多 →
PyTorch LSTM 多变量多任务预测:3种损失函数加权策略对比与代码实现

PyTorch LSTM 多变量多任务预测:3种损失函数加权策略对比与代码实现

PyTorch LSTM 多变量多任务预测:3种损失函数加权策略对比与代码实现在时间序列预测领域,多任务学习正逐渐成为提升模型性能的关键技术。当我们需要同时预测多个相关变量时,传统的单任务学习方法往往难以捕捉变量间的复杂关系。本文将深入探讨…

2026/7/6 9:04:10 阅读更多 →
Python Selenium自动化校园网登录:从环境搭建到打包部署全攻略

Python Selenium自动化校园网登录:从环境搭建到打包部署全攻略

1. 项目概述与核心价值 校园网登录,尤其是像深澜Srun这类需要跳转认证页面的系统,对于需要长时间保持网络连接的用户来说,是个不大不小的痛点。手动打开浏览器、输入账号密码、点击登录,这套流程每天重复几次,不仅繁琐…

2026/7/6 9:04:10 阅读更多 →
Python自动化破解WIPO六宫格验证码:图像处理与网络请求实战

Python自动化破解WIPO六宫格验证码:图像处理与网络请求实战

1. 项目概述与核心痛点最近在做一个专利数据分析的小工具,需要从世界知识产权组织(WIPO)的专利数据库里批量抓取一些公开数据。本来以为用Python的requests库写个脚本,模拟登录后按部就班爬取就完事了,结果第一步就卡在…

2026/7/6 9:02:09 阅读更多 →
基于MCP协议为AI编程助手集成逆向分析能力:从静态反编译到动态Hook的完整实践

基于MCP协议为AI编程助手集成逆向分析能力:从静态反编译到动态Hook的完整实践

1. 项目概述:当AI编程助手学会“拆解”程序 想象一下,你正在分析一个加密的Android应用,试图找到它的网络通信密钥。传统流程是:打开IDA Pro反汇编引擎库,在十六进制视图中搜索可疑字符串;同时启动Frida&am…

2026/7/6 9:00:06 阅读更多 →
AI模型测试:从确定性断言到统计判定的工程实践

AI模型测试:从确定性断言到统计判定的工程实践

1. 项目概述:当AI的“不确定性”撞上测试的“确定性” 干了这么多年测试,从功能、性能到安全,感觉什么大风大浪都见过了。但最近两年,随着公司业务里AI驱动的功能越来越多,比如智能客服的意图识别、内容平台的个性化推…

2026/7/6 9:00:06 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻