news 2026/11/2 2:02:03

2020微服务电商代码包跑通:启动顺序、下单链路与事务避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2020微服务电商代码包跑通:启动顺序、下单链路与事务避坑

简介:面向具备Java与Spring Cloud基础、希望从单体应用迈向分布式架构的后端开发者,这套2020版谷粒商城项目代码以微服务电商为主线,把服务拆分、注册发现、负载均衡、API网关、分布式事务、消息队列、分库分表、分布式缓存、监控日志及CI/CD等知识点放入可联调的商城场景,帮助读者理解高可用、可扩展平台的设计取舍。资源以zip压缩包提供,体积约26.32MB,文件总数与具体类型明细上游暂未披露,解压后可按用户、商品、订单、支付等服务模块对照架构图和高级篇图谱梳理调用链路。已有1584人学习,适合课程复盘、毕业设计选题参考或搭建微服务练习环境;通过阅读代码与模拟联调,还能积累接口划分、异步解耦、缓存治理和问题排查的实战思路,对准备分布式面试场景题亦有参考价值。

1. 拿到一套 2020 年的微服务电商代码包,先别急着全量 import

解压之后你会看到十几个 Spring Boot 模块、两套前端、一堆中间件配置散在 resources 里。多数人第一步是打开 IDEA 全量导入,等二十分钟 Maven 拉完依赖,最后卡在注册中心连不上、网关 503,人就开始怀疑代码是不是坏的。真实情况往往相反:这套代码能不能跑起来,八成不取决于代码逻辑,而取决于你有没有按它的顺序把中间件一个个摆到位。它要解决的是很具体的一件事——把单体商城的商品、库存、订单、购物车、检索拆成独立进程,跨服务用 Feign 和消息队列串起来,让一个初学者能看见一次下单到底穿过了哪些服务、在哪一步失败、失败了怎么回滚。适合已经写熟 CRUD、想搞明白分布式调用链和事务边界的人;只想要个后台管理模板的,这套东西对你是纯负担。

2. 拆包先看骨架:服务域划分与中间件启动顺序

2.1 目录即架构:从模块清单反推服务边界

别一上来读 Controller,先ls根目录。模块名基本就是服务边界,命名里已经写清楚了哪些是业务域、哪些是基础设施。我一般先做一张表,把「必须起」和「可以先放」分清楚,起环境时能省掉一半的启动失败。

模块职责起步阶段是否必须
common统一返回体、全局异常、常量、用户上下文必须,所有模块都依赖它
gateway路由、跨域、鉴权前置必须,前端只认它
auth登录、令牌签发必须,否则拿不到身份
member会员、积分、收货地址必须,是身份数据来源
productSPU/SKU、属性、品牌分类必须,详情页数据源头
ware仓库、库存、采购单必须,下单要扣它
order订单、订单项、状态机必须
cart购物车可选,接口能先打桩
coupon优惠券、满减可选
search基于 ES 的商品检索可选,索引不建也能跑
third-party对象存储、短信可选,配置留空即可

为什么这么分?因为分布式拆分的第一个判断标准不是「代码量」,而是「数据归属」和「变更频率」。商品数据读多写少、库存数据强一致要求高、订单数据只增不改,这三类放一个库里但分不同服务,是最常见的教学式拆法,代价是跨库事务必须自己处理。看懂这一点,后面 Seata 那一章你就不会觉得是硬塞进来的技术。

common 模块里有两样东西值得单独翻出来看:一是统一返回体R,二是基于ThreadLocal的用户上下文持有类。前者决定你调试时看到的错误码长什么样,后者是后面很多玄学问题的源头——异步线程里取不到当前登录用户,十有八九就是它。

2.2 版本对齐:从父 pom 反查 BOM,别抄网上的版本组合

这类项目的依赖冲突,99% 出在 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者的对应关系上。别去网上搜「某某版本组合」,直接在父 pom 里看它锁了什么。一条命令就能把关键版本捞出来:

# 在代码包根目录执行,一次性看清父 pom 里锁定的版本线 grep -nE "spring-boot|spring-cloud|alibaba|nacos|sentinel|seata|rabbit|elasticsearch" pom.xml | head -50 # 再看每个子模块有没有自己偷偷改版本(这是最常见的翻车点) find . -name "pom.xml" -not -path "*/node_modules/*" \ | xargs grep -l "spring-boot-starter-parent" 2>/dev/null

第一段命令输出的是父级依赖管理,标准的写法是dependencyManagement里 import 两个 BOM:一个 Spring Cloud,一个 Spring Cloud Alibaba,具体版本号以你包里的为准。第二段是排查用——如果某个子模块自己又声明了一遍 parent,它就会脱离统一版本管理,注册中心客户端版本和主工程对不上,表现就是服务能起来但注册不上,或者能注册上但配置拉不下来。

参数上你只需要盯三处:Nacos 客户端版本决定要不要额外开 9848 端口(2.x 客户端需要,1.x 不需要);Sentinel 的 dashboard 地址写在配置里,端口默认 8080,和你的网关端口撞了就得改;Seata 的版本决定了配置是走file.conf还是application.yml,1.4 之后基本都往 yml 迁了,混着配就是启动报错。

2.3 中间件启动顺序与最小启动集

顺序错了,你会看到一堆「连接超时」但根本不知道是谁的问题。我一般按「存储 → 注册配置 → 消息 → 检索 → 服务」这个顺序起,每一步都单独验证一次,别一次性全拉起。

# 1. MySQL:先建库再导数据,脚本一般在 sql/ 目录下 docker run -d --name mall-mysql -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -v $PWD/sql:/docker-entrypoint-initdb.d \ mysql:5.7 --character-set-server=utf8mb4 # 2. Redis:商城缓存、购物车、分布式锁都靠它 docker run -d --name mall-redis -p 6379:6379 redis:6 redis-server --appendonly yes # 3. Nacos:单机模式启动,必须带 MODE=standalone,否则默认集群模式起不来 docker run -d --name mall-nacos -p 8848:8848 -p 9848:9848 \ -e MODE=standalone nacos/nacos-server:1.4.1 # 4. RabbitMQ:延迟队列插件要单独开,库存解锁靠它 docker run -d --name mall-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.8-management # 5. Elasticsearch:单节点必须关掉生产检查,否则直接退出 docker run -d --name mall-es -p 9200:9200 \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" elasticsearch:7.9.0

每起一个都回头验证一次:MySQL 用mysql -uroot -p -e "show databases"看库在不在;Nacos 直接访问 8848 控制台,能登进去再往下走;RabbitMQ 访问 15672 看队列页签能不能打开;ES 用curl localhost:9200/_cluster/health,返回green或yellow都算正常,red说明分片分配失败。全部就绪之后再逐个起 Java 服务,启动顺序是 common 装进本地仓库、然后是 member/auth、product/ware、order、最后 gateway。网关一定最后起,因为它一启动就会去拉路由和鉴权,前面的服务没注册上,它会一直刷错误日志,把真正的问题淹掉。

3. 跑通主链路:从商品详情到下单扣库存

3.1 商品详情页:缓存穿透、空值兜底与分布式锁

详情页是整个系统里读压力最大的地方,缓存没做好,压测第一分钟就能把 MySQL 打满。这类代码包里通常已经写了缓存逻辑,但大概率只做了「查缓存 → 查库 → 写缓存」,漏了空值和并发重建两件事。我一般在ProductService的详情方法里补成这个结构:

public SkuDetailVO getSkuDetail(Long skuId) { String cacheKey = "sku:detail:" + skuId; String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { // 空字符串是"查过但库里没有"的哨兵值,防止同一个不存在的 id 反复打到库 return "".equals(cached) ? null : JSON.parseObject(cached, SkuDetailVO.class); } RLock lock = redissonClient.getLock("lock:sku:detail:" + skuId); try { // waitTime 5s 是排队上限,leaseTime 10s 是持锁上限 // leaseTime 传 -1 会启用看门狗自动续期,这里显式给值避免业务卡死时锁不释放 if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { // 双重检查:拿到锁的瞬间别人可能已经重建好了 cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return "".equals(cached) ? null : JSON.parseObject(cached, SkuDetailVO.class); } SkuDetailVO db = skuMapper.selectDetail(skuId); if (db == null) { // 空值缓存 TTL 一定要短,60s 足够挡住攻击,又不至于脏太久 redisTemplate.opsForValue().set(cacheKey, "", 60, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(db), 30, TimeUnit.MINUTES); return db; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 只释放自己持有的锁,避免误删别人的 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } return null; }

逻辑上有三个关键点。第一是空值哨兵,""和null的差别在于 Redis 存不了null,用空串表达「确认不存在」,配合 60 秒短 TTL,热 key 攻击就落不到数据库上。第二是双重检查,不加的话在锁排队期间会有多线程重复回源。第三是isHeldByCurrentThread判断,leaseTime到期锁自动释放后,原线程再去unlock会抛异常,这个判断能挡住。

参数上,leaseTime别设太长也别设太短。设 30 秒以上,一旦业务线程卡住,锁会长时间不释放;设 2 秒以内,重建逻辑稍微慢一点锁就过期了,双重检查也救不了并发回源。10 秒是我在本地和测试环境都比较稳的一个值,生产上得按实际 DB 查询耗时调。

3.2 下单接口:Feign 超时、重试与库存锁定

下单是典型的跨服务链路:订单服务调会员服务查地址、调库存服务锁库存、调优惠券服务核销。这条链路上最容易忽略的是超时配置,默认值偏大,一个慢调用就能把网关线程池耗干。

feign: client: config: default: connectTimeout: 2000 # 建连 2s,内网正常几十毫秒,超过就是网络或服务没起 readTimeout: 3000 # 读超时 3s,扣库存这种写操作不宜超过它 loggerLevel: basic ware-service: # 针对单个服务单独覆盖,扣库存比查地址更重要 readTimeout: 5000 httpclient: enabled: true # 用连接池替代默认 HttpURLConnection,否则高并发下连接耗尽是常态 max-connections: 500 max-connections-per-route: 100 # 关闭 Feign 的自动重试,重试在写操作上等于重复下单 ribbon: MaxAutoRetries: 0 MaxAutoRetriesNextServer: 0

对应的接口声明要注意注解,跨服务传参丢字段是高频问题:

@FeignClient(name = "ware-service", fallbackFactory = WareFallbackFactory.class) public interface WareFeignService { // 单参数也必须写 @RequestParam,不写的话服务端收到的是 null @GetMapping("/ware/sku/hasStock") R<Boolean> hasStock(@RequestParam("skuId") Long skuId); // 多参数同理,且 GET 传对象要加 @SpringQueryMap,否则会被当成请求体转成 POST @PostMapping("/ware/lock") R<Long> lockStock(@RequestBody LockStockDTO dto); // 查询类接口适合加降级,写操作不要加 fallback,失败了必须让上层感知 @GetMapping("/ware/wareinfo/list") R<List<WareVO>> listWare(); }

第一个方法如果不加@RequestParam("skuId"),Feign 拼出来的 URL 里就没有这个参数,服务端收到 null,库存校验直接放行,压测时表现为「库存扣成负数但没有任何报错」,是那种能查一整天的血泪经验。第二个方法的@RequestBody两边必须对齐,服务端漏写会报 415。至于fallbackFactory,只给查询接口配降级返回空列表是合理的,写接口配降级等于把失败伪装成成功,这个边界要守住。

3.3 库存扣减:乐观锁、幂等与重复扣减的边界

库存是这套系统里唯一必须保证强一致的地方。代码包里的常见写法是先select再update,中间隔着一次网络往返,并发下必然超卖。改成带条件的单条更新,把判断交给数据库:

-- 一条语句完成"校验 + 扣减",影响行数为 0 就代表库存不足 UPDATE wms_ware_sku SET stock_locked = stock_locked + #{num} WHERE sku_id = #{skuId} AND ware_id = #{wareId} AND stock - stock_locked >= #{num};

配套的 Java 层要处理影响行数,并加一层幂等,防止消息重投或用户重复点击时扣两次:

@Transactional(rollbackFor = Exception.class) public Long lockStock(LockStockDTO dto) { // 幂等表:唯一索引 (order_sn, sku_id),重复插入直接失败走返回 int inserted = lockLogMapper.insertIgnore(dto.getOrderSn(), dto.getSkuId(), dto.getNum()); if (inserted == 0) { return lockLogMapper.selectIdByOrderSn(dto.getOrderSn()); } int rows = wareSkuMapper.lockStock(dto.getSkuId(), dto.getWareId(), dto.getNum()); if (rows == 0) { // 抛出去交给全局异常处理,订单侧收到明确失败就回滚 throw new BizException("库存不足"); } return lockLogMapper.selectIdByOrderSn(dto.getOrderSn()); }

where stock - stock_locked >= #{num}这一句是整个扣减的核心,它利用了 InnoDB 行锁,把并发串行化在数据库层,不需要分布式锁也不会超卖。代价是热点 SKU 上行锁竞争激烈,QPS 上不去。真要做秒杀量级,得把库存预热到 Redis、用 Lua 脚本原子扣减,再异步落库,那是另一个工程量级的事。

幂等表这里的坑在于:它和业务扣减必须同一个本地事务。如果幂等记录提交了、扣减回滚了,重试时就会误判为「已处理」而直接返回,库存永远少扣一次。@Transactional加在方法上、两个操作走同一个数据源,才能保证一起成功或一起失败。

4. 分布式事务与消息可靠性:三条链路的参数怎么定

4.1 Seata AT 模式:undo_log 与全局锁的两个必查项

AT 模式用起来最省事,注解一加就行,但它对库有硬性要求——每个参与事务的业务库都必须建undo_log表,少一个就在提交阶段报错,而且报错信息指向的是另一个库,非常难定位。

CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 DDL 要在 order、ware、coupon 三个库各执行一遍,一张都不能少。配置侧通常只需要三行:

seata: enabled: true application-id: order-service # 必须和注册到 Nacos 的服务名一致 tx-service-group: mall_tx_group service: vgroup-mapping: mall_tx_group: default # 集群名要和控制端的 default 对上 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: ""

application-id和服务名不一致是最隐蔽的一类问题:事务能开,但回滚时控制端找不到分支,日志里只有一行「no available service」,服务本身看不出任何异常。tx-service-group到vgroup-mapping这层映射也必须成对,写错一个字母就退化成「本地事务看起来成功了,实际跨库没回滚」。

4.2 可靠消息:confirm 回调与消费端幂等表

订单超时未支付要解锁库存,链路天然异步,用延迟队列比分布式事务更合适。但消息发出去和本地事务提交之间有个经典的时间窗——事务还没提交消息已经投递,消费者去查订单查不到。可靠消息的写法是把「发消息」挂到事务提交之后:

@Transactional(rollbackFor = Exception.class) public void createOrder(OrderVO vo) { orderMapper.insert(vo); // 注册事务同步器,只有本地事务真正提交后才投递 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", vo.getOrderSn(), message -> { // 延迟 30 分钟,单位毫秒,需要队列声明 x-delayed-type message.getMessageProperties().setDelay(30 * 60 * 1000); message.getMessageProperties().setMessageId(vo.getOrderSn()); return message; }); } }); }

同时在配置里打开发送端确认,否则消息投递到交换机失败你完全不知道:

spring: rabbitmq: publisher-confirm-type: correlated # 开启 confirm 回调,能拿到 ack/nack publisher-returns: true # 路由失败时回调 return template: mandatory: true # 这一项不开,return 回调永远不会触发 listener: simple: acknowledge-mode: manual # 手动 ack,消费失败可以 nack 重入队 prefetch: 10 # 每个消费者预取条数,别设太大否则消息堆在内存里

mandatory: true是最容易漏的一项。不开的话,交换机收到消息但没匹配到队列,消息会被直接丢弃,而publisher-returns那个回调永远不会执行,你会在「已发送」的日志里安稳地丢掉几十条库存解锁消息。消费端则要落一张mq_message_log表,用messageId做唯一索引,重投时插入失败就直接 ack,别让重复消费穿透到业务层。

4.3 身份透传:ThreadLocal 在异步线程里失效怎么办

网关鉴权后把用户 id 塞进请求头,下游服务用拦截器取出来写进ThreadLocal。这套机制在同步链路里没问题,一旦进了@Async线程池或者 CompletableFuture,取出来就是 null,表现为「明明登录了却提示未登录」。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { String userId = req.getHeader("X-User-Id"); if (StringUtils.hasText(userId)) { UserContextHolder.set(Long.valueOf(userId)); } return true; } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) { // 必须清理,Tomcat 线程复用,不清理就是上一个用户的数据串到下一个请求 UserContextHolder.remove(); } }

afterCompletion里的remove()不是可选项。线程池复用下,一次没清理,后面所有落在这个线程上的请求都会拿到别人的身份,这类问题在测试环境很难复现,上线后就是数据越权。异步场景的补法有两种:一是给线程池套一层TaskDecorator,提交任务时把上下文快照传过去;二是干脆不在异步线程里依赖ThreadLocal,把 userId 作为参数显式传进方法。后者笨但可靠,我一般优先选它。

5. 避坑与排查:五类跑不起来的现场

5.1 服务能起来但注册列表是空的

现象是启动日志没有报错,Nacos 控制台却看不到实例。九成是版本问题:客户端是 2.x 而服务端是 1.x,或者反过来,gRPC 端口 9848 没映射。先看启动日志里有没有register fail或connect timeout,再确认容器端口映射里有没有 9848。如果是 IP 问题——容器内注册的是内网地址,宿主机访问不到——在配置里显式指定spring.cloud.nacos.discovery.ip。改完别急着重启全部,只重启那一个服务,看列表有没有变化。

5.2 网关启动直接报 Bean 冲突

现象是启动时抛Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway。原因是某个子模块或者公共模块里引了spring-boot-starter-web,传递到了网关。网关基于 WebFlux,两者不能共存。解决是在网关模块的依赖里把 web 排除掉:

<dependency> <groupId>com.example</groupId> <artifactId>mall-common</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency>

更彻底的做法是把 common 拆成 common-core 和 common-web 两个模块,网关只引 core。

5.3 跨域配置写了但预检请求还是 403

现象是前端OPTIONS请求返回 403,GET却正常。原因是网关里用CorsWebFilter配了跨域,但鉴权过滤器把OPTIONS也拦了。解决是让鉴权过滤器放行预检请求,判断HttpMethod.OPTIONS直接放行,别去校验令牌。另外注意allowedOrigin别写*同时开allowCredentials,浏览器会直接拒绝,得写具体域名。

5.4 商品检索搜不到中文

现象是 ES 里明明有数据,搜品牌名却一条不返回。原因是索引创建时没指定分词器,默认的 standard 分词器把中文按单字切,手机被切成手和机。解决是在建索引的 mapping 里给需要检索的字段指定analyzer: ik_max_word、search_analyzer: ik_smart,并确认 ES 容器里装了这个插件。已经建好的索引改 mapping 不生效,必须删了重建,这一步会丢数据,所以要先把商品数据重新灌一遍。

5.5 下单偶发「库存不足」但实际有货

现象是压测时大约百分之几的请求报库存不足。按顺序查三处:一是 Feign 参数有没有加注解,skuId传成 null 会让条件里的sku_id = null永远不成立;二是stock_locked字段是不是有历史脏数据,导致stock - stock_locked算出来是负数;三是幂等表里是不是有上次失败留下的记录,insertIgnore返回 0 就直接返回了旧结果,根本没走到扣减。第三条最坑,因为它看起来像「扣减逻辑没执行」,实际是幂等命中。排查时先把幂等表按orderSn查一遍,比看代码快。

6. 二次开发验证:改一处业务,跑一遍全链路

拿到这类代码包,真正有价值的动作不是「跑起来」,而是「改一处再跑一遍」。我一般挑最小的改造点:给商品加一个自定义字段,从 SKU 表加列、到product服务加映射、到详情页 VO 加字段、到前端详情页渲染出来,四个位置都要改。这条线走通,说明你对这套项目的分层和数据流已经摸清了。

验证顺序建议按依赖反向走。先改库表,重启product服务,用curl直接打它的接口确认字段出来了;再经过网关打一次,确认路由和鉴权没拦;最后打开前端页面看渲染。跳过中间任何一步,出问题时就分不清是服务没生效、网关没转发还是前端没刷新。压测环节用 JMeter 跑详情页和下单两个场景,各 200 并发、持续 5 分钟,重点盯三个指标:详情页的 P99 响应时间、下单接口的错误率、Redis 的命中率。P99 超过 500ms 先查缓存有没有穿透,错误率超过 1% 先看 Feign 超时日志。

还有一件容易被忽略的事:把每个服务的logging.level调成debug之前,先确认日志不会把ThreadLocal里的用户信息打出来。这套代码里很多拦截器会打印请求头,生产环境直接暴露身份字段。

第一次跑这套东西的时候,我图省事把五个中间件一次性全docker run起来,然后看到七八个服务同时启动,日志刷得飞快,最后网关报了一屏 503,我盯着路由配置改了两个小时,实际原因只是 Nacos 那时还没起来、服务根本没注册上。从那以后我每次起这套环境,都强制走一遍「起一个 → curl 验一个 → 再起下一个」的流程,宁可多花十分钟,也不要在几百行日志里猜。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 1:59:27

谷歌让太空里的AI 回了一首诗,然后呢?

谷歌把AI芯片送上了太空&#xff0c;然后让它写了一首诗。这听起来像一场昂贵的浪漫实验。但对谷歌来说&#xff0c;诗写得好不好&#xff0c;可能是整件事里最不重要的部分。10月30日&#xff0c;谷歌公布&#xff1a;Project Suncatcher原型卫星搭载的数据中心级TPU&#xff…

作者头像 李华
网站建设 2026/11/2 1:59:06

唤醒工作台全实录(六):数据层——5ms 竞态窗口与“查询即引用“

第五幕的表针停在 9 月 21 日。这一幕先把它拨回八月中旬——云侧提示词一版版重写的同时&#xff0c;端侧数据层动了两场设计手术&#xff1b;再把表针拨到 9 月 22 日晚上&#xff0c;那里有一场无声的故障等着。 1. 查询即引用 8 月 13 日&#xff0c;数据模型重构的讨论从盘…

作者头像 李华
网站建设 2026/11/2 1:58:55

从千行到6行:PAO压缩PHP测试输出的震撼前后对比

【免费下载链接】pao PAO is agent-optimized output for PHP testing tools. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/pao1/pao 点击查看 免费下载 当你用 AI Agent 跑 PHP 测试时&#xff0c;满屏的进度点和冗长日志不仅白白消耗 Token&#xff0c;还会干扰 A…

作者头像 李华
网站建设 2026/11/2 1:58:18

九.数据结构——哈希表(2)

哈希表&#xff08;1&#xff09;中我们简单的认识了一下哈希表的相关内容&#xff0c;本章&#xff0c;让我们一起完成哈希表的构建和相关操作的代码实现吧&#xff01;代码&#xff1a;0.结构体的定义&#xff08;hash表和hash桶节点&#xff09;//hash表的默认负载因子 #def…

作者头像 李华
网站建设 2026/11/2 1:56:52

Vim 学习速成:理解快捷键

Vim 学习速成 一、Vim 的优势 Vim 通过全键盘快捷键完成操作&#xff0c;长期使用效率高于依赖鼠标的编辑器&#xff08;VS Code、Sublime Text 等&#xff09;。类比竞技游戏&#xff1a;高阶操作绑定快捷键&#xff0c;规避鼠标移动带来的延迟&#xff1b;Vim 不需要做到全…

作者头像 李华