news 2026/10/21 17:45:57

PostgreSQL分区表实战:2200万行订单流水优化全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL分区表实战:2200万行订单流水优化全记录

这个标题我相信所有PostgreSQL的运维和老开发看了都会心一笑——不是因为它简单,恰恰是因为这个功能看着入门简单,真正上了生产环境、扛住真实业务流量之后,各种问题才开始浮现。我手上负责的某电商订单系统,订单流水表一度涨到了两千多万行,单表查询从毫秒级退化到秒级,夜间批量任务跑起来能把IO打满,后来花了两周时间把核心流水表改成分区表,才把这口气喘过来。

这篇文章就围绕这个真实改造过程展开。我会讲清楚当时为什么选分区表而不是分库分表,分区方案具体怎么设计,迁移数据时哪些步骤不能省,上线后又踩了哪些坑,以及分区表的日常维护到底怎么做。不写空泛的理论,全程都是可以直接抄作业的SQL、参数和排查思路。

1. 一张日增3万行的订单流水表,是如何一步步“变卡”的

先说一下当时的业务背景。项目是一个标准的分销型电商系统,用户下单后会生成订单主记录和一系列流水记录:支付回调、退款、售后状态变更、风控日志,全部落在同一张名为sim_order_log的表里。

这张表早期的结构其实没什么问题:

CREATE TABLE sim_order_log ( id BIGSERIAL PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, biz_type SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), payload JSONB, status SMALLINT NOT NULL DEFAULT 1 ); CREATE INDEX idx_order_log_user_created ON sim_order_log (user_id, created_at DESC);

流量没起来之前,一切都很正常,查询走索引,响应在几十毫秒级别。但业务跑了一年半之后,这张表的数据量稳定在了2200万行左右,加上JSONB字段和索引,表空间加索引空间接近6GB。问题开始集中爆发:

  • 订单详情页要查某个订单的所有流水,单条SQL本身不复杂,但走idx_order_log_user_created时索引深度增加,随机IO变多,接口P99从80ms涨到了1.2秒。
  • 每天凌晨的归档任务要把超过90天的流水搬到历史库,用的是DELETE FROM sim_order_log WHERE created_at < now() - interval '90 days',这条语句高峰期能跑40分钟,期间产生大量WAL日志,还和业务写入抢IO。
  • 因为高频的插入和删除,表膨胀明显,autovacuum 经常跑不完,手动 vacuum 又会锁表,导致夜间告警不断。
  • 索引维护成本越来越高,每次REINDEX都是对线上的一轮冲击。

其实到这个时候,团队里讨论过几个方案:一是上分布式数据库,迁移成本太高,得改数据访问层;二是做分库分表,但中间件带来的复杂度对一个小团队并不友好;三是继续扛着,定时清理旧数据。最后还是决定,先用PostgreSQL原生分区表,把这张表内部拆开。原因很简单:大部分慢查询都带有created_at时间条件,而旧数据归档本质是删除时间较早的行,分区表在这两个场景下都有天然优势。

2. 分区方案设计:为什么选范围分区,分区键和粒度怎么定

选型阶段,我们把分区方案的核心问题拆成了三个:分区类型、分区键、分区粒度。

2.1 业务查询模式决定了必须用范围分区

PostgreSQL 从10版本开始原生支持声明式分区,分区类型包括范围分区、列表分区和哈希分区。我们业务上的所有高频查询,几乎都带时间窗口:查某个用户的近期流水、统计某天的支付成功率、跑凌晨归档……

这类“按时间段圈数据”的查询,最适合范围分区。每月的订单流水落入一个独立分区,查询时优化器根据created_at条件直接裁剪掉无关分区,只扫目标分区,这就是分区裁剪的核心收益。

哈希分区当时也考虑过,但它适合按某个键分散写入的场景,比如把数据平均打散到多个物理分区。我们的数据写入本来就是顺序增长的,哈希分区不仅没法优化时间范围查询,还会让“查单个月数据”变成“扫全部哈希分区”,属于反向优化。

列表分区更不用说,业务上没有明显的枚举维度和数据生命周期绑定,直接排除。

2.2 分区键不能拍脑袋,我踩过一次时间格式的坑

分区键的选择上,最稳妥、最贴合业务查询的就是created_at。但这里有一个非常容易被新手忽略的细节:我们表里的created_at是TIMESTAMPTZ,不是TIMESTAMP。

TIMESTAMPTZ存的是带时区的时间,实际存储是以UTC形式保存的。如果分区函数直接写成to_char(created_at, 'YYYYMM'),在时区偏移的影响下,某些凌晨的订单会被划分到前一个分区,读取的时候用本地时间边界去查,就会发生“数据确实存在,但查不到或者落在默认分区”的诡异问题。

我们的做法是统一在分区函数里做时区归一化:

to_char(timezone('UTC', created_at), 'YYYYMM')

所有写入和查询的SQL都强制走同一个函数,保证分区判定逻辑一致。后来规范里也定了死规矩:涉及分区键的SQL,不允许直接裸用created_at做比较后再让应用层去拼分区名,统一走触发器或应用层的时间工具类。

2.3 按月分区是“细粒度”和“分区数量”的平衡点

分区粒度上,我们测试过按周、按月、按季度三种方案。

按周分区,一年52个分区,好处是删除旧数据时DROP PARTITION的粒度更细,坏处是分区数量增长快,PG的系统目录和统计信息负担变大,而且很多报表查询是按月统计的,跨分区查询要拼接好几个分区,性能反而不如按月。

按季度分区,分区数量少,但单分区数据量仍然过大,删除一个季度的分区相当于一次性删掉几百万行,灵活性不够。

最后定了按月分区。每个分区约90万到120万行,单个分区文件大小控制在几百MB,索引查询、vacuum、归档都很舒服。预创建策略上,我们每次提前创建未来3个月的分区,避免12月31日晚上跨年时应用还在往12月分区写数据、凌晨任务又去建1月分区的竞态问题。

2.4 声明式分区和“继承式分区”的取舍

说句实话,PG10之前的老项目里大量使用的继承式分区,我也维护过。通过INHERITS创建子表、用触发器把数据分发到各子表、查询时靠constraint exclusion实现裁剪,这套东西能用,但有一个致命弱点:父表上的约束和索引不会自动同步到子表,每次加索引都要手动给所有子表执行一遍,漏一个子表就是一次事故。

PG10以后的声明式分区,父表和子表的关系由内核管理,索引、约束、默认值都可以通过父表统一下发,而且ATTACH/DETACH可以在线操作,不用长时间锁表。所以我们的方案毫不犹豫地用了声明式分区。

3. 分区改造的完整实施步骤:从建父表到回填数据

这一节是全文最“实操”的部分。我把当时执行的步骤按顺序列出来,每一步都附了理由和注意事项,确保你照着做不会中途卡壳。

3.1 第一步:重建为分区父表

我们迁移策略不是ALTER TABLE直接把普通表转成分区表(早期版本不支持这种在线转换),而是新建一张分区父表,然后数据回填,再切换应用。父表定义如下:

CREATE TABLE sim_order_log ( id BIGSERIAL NOT NULL, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, biz_type SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), payload JSONB, status SMALLINT NOT NULL DEFAULT 1 ) PARTITION BY RANGE (timezone('UTC', created_at));

注意几点:

  • 原表的PRIMARY KEY没有直接搬过来。因为PostgreSQL要求,分区表的唯一约束必须包含分区键。如果把id直接设为唯一约束,而id不是分区键,会直接报错。所以主键改成了(id, created_at)联合约束,这在应用层没有任何影响,因为我们所有的查询都是带时间条件的。
  • 分区键用了timezone('UTC', created_at)表达式。PG11及以下不支持表达式作为分区键,当时我们线上的版本是PG12,所以没问题。如果你还在PG10,需要用created_at TIMESTAMPTZ NOT NULL直接做分区键,但要提前在应用层把时间统一为UTC。

3.2 第二步:建立各个月份分区

我们手工创建了一张模板SQL,每个月自动生成并执行:

CREATE TABLE sim_order_log_202401 PARTITION OF sim_order_log FOR VALUES FROM ('2024-01-01 00:00:00+00') TO ('2024-02-01 00:00:00+00');

起点和终点都是UTC时间。这样就保证任何时区的客户端写入,只要底层值在区间内就能落到正确分区。

同时创建了一个默认分区,用来接住因为各种原因没有跑到正确分区去的“流浪数据”:

CREATE TABLE sim_order_log_default PARTITION OF sim_order_log DEFAULT;

这个默认分区是我强烈建议创建的,否则一旦有程序没用统一时间函数、或者时间边界计算错误,插入时会出现“no partition of relation found for row”的报错。虽然默认分区会导致数据悄悄堆在不该在的地方,但至少不会阻断写入。配合监控定期检查默认分区行数,能第一时间发现Bug。

3.3 第三步:创建分区自动创建函数

如果每次都要手工建分区,运维会崩溃。我写了一个简单的自动建分区函数,供定时任务调用:

CREATE OR REPLACE FUNCTION auto_create_log_partition(target_date date) RETURNS void AS $$ DECLARE partition_name text; start_date text; end_date text; BEGIN -- 目标日期的下个月 partition_name := 'sim_order_log_' || to_char(target_date, 'YYYYMM'); start_date := to_char(target_date, 'YYYY-MM-01'); end_date := to_char(target_date + interval '1 month', 'YYYY-MM-01'); IF NOT EXISTS ( SELECT 1 FROM pg_class WHERE relname = partition_name ) THEN EXECUTE format( 'CREATE TABLE %I PARTITION OF sim_order_log FOR VALUES FROM (%L) TO (%L)', partition_name, start_date || ' 00:00:00+00', end_date || ' 00:00:00+00' ); END IF; END; $$ LANGUAGE plpgsql;

函数逻辑很简单:根据传入日期生成下个月分区名和时间边界,如果表不存在就创建。后面接上pg_cron,每月1号和每月15号各执行一次,预创建未来3个月的分区。

3.4 第四步:回填历史数据

这一步是整个迁移里最磨人的。2200万行数据,如果用一条INSERT INTO sim_order_log SELECT ... FROM 旧表一把梭,会带来两个问题:

  • 大事务会锁表,业务写入被迫长时间等待;
  • 产生的WAL量巨大,主从同步延迟会拉到分钟级以上。

我们的做法是按天切片,分批回填。每天大概3万行,每批最多更新5万行,然后提交一次。用一个循环脚本控制:

-- 伪代码,实际通过脚本循环执行 SELECT min(created_at), max(created_at) FROM legacy_sim_order_log; -- 对每个日期 d: INSERT INTO sim_order_log (id, order_no, user_id, biz_type, created_at, payload, status) SELECT id, order_no, user_id, biz_type, created_at, payload, status FROM legacy_sim_order_log WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02' ORDER BY id;

每批之间间隔1秒到2秒,给主库喘息的时间。同时把归档任务的夜间时间段和回填时间错开。回填过程中我们实时观察主从延迟和IO等待,整体跑了一天半,没有出现长时间锁表问题。

3.5 第五步:统一创建索引

父表创建完后,在父表上统一创建索引,所有现存分区和未来新分区都会自动带上:

CREATE INDEX idx_order_log_user_created ON sim_order_log (user_id, created_at DESC); CREATE INDEX idx_order_log_order_no ON sim_order_log (order_no); CREATE INDEX idx_order_log_biz_type ON sim_order_log (biz_type);

这里特别强调一点:CONCURRENTLY关键字不能用在分区父表上。我当时为了不锁写入,试过CREATE INDEX CONCURRENTLY,结果PG直接报错,说不支持在分区父表上并发建索引。解决办法是先建父表索引(短暂锁表,因为当时回填还没开始,影响可控),后续子表如果需要单独加索引,再用CONCURRENTLY落在具体子表上。

3.6 第六步:切换应用写入路径

索引建好、数据回填完成之后,就是应用层切换。我们的做法是:把sim_order_log改名为sim_order_log_legacy,新表命名sim_order_log。应用层代码一行不改,因为表名没变,只是底层变成了分区表。

切换前我特意观察了旧表上的序列(sequence)值:

SELECT last_value, max(id) FROM sim_order_log_legacy_seq, sim_order_log_legacy GROUP BY last_value;

要确保新表里的BIGSERIAL序列起点大于旧表最大ID,否则插入时主键冲突。一般直接:

SELECT setval('sim_order_log_id_seq', (SELECT max(id) FROM sim_order_log_legacy));

这一步忘了做的话,线上开工第一天就会爆主键冲突。

切换后立刻执行验证SQL:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM sim_order_log WHERE user_id = 12345 AND created_at >= '2024-01-01' AND created_at < '2024-02-01';

看到的结果是:Seq Scan on sim_order_log_202401或Index Scan on idx_order_log_user_created,确认分区裁剪生效,没有走全分区扫描。

4. 改造后的真实收益:三个维度的数据对比

分区表上线后,我记录了两个月的运行数据。这里不说“感觉快了”这种话,直接看数字。

4.1 旧数据归档:从40分钟降到秒级

这是分区表带来的最直接、最夸张的收益。以前归档90天前的数据,靠的是DELETE一条条清理,现在直接:

ALTER TABLE sim_order_log DETACH PARTITION sim_order_log_202312;

把整个12月分区分出去,然后在低峰期DROP TABLE或转成归档表。单次操作秒级完成,不产生海量WAL,不会拖垮主库。12月分区大约100万行,对比一下:

操作方式耗时WAL产生量锁表情况
DELETE FROM ... WHERE created_at < ...40分钟极大,主从延迟升高长时间持有行锁
DETACH + DROP PARTITION不到1秒极小短暂锁表

从这以后,归档任务从“每天夜间高危操作”变成了“定时分区摘除”,再也没因为清理旧数据出过事故。

4.2 典型查询:P99从1.2秒降到150毫秒

订单详情页要查询某个用户最近半年的流水,SQL条件同时包含user_id和created_at。分区前扫的是一个6GB的单表B树,分区后裁剪到了6个月份分区,实际扫描的数据量只有原来的四分之一左右,P99从1.2s降到150ms。

没带created_at条件的查询呢?我专门测过,性能反而会下降,因为优化器要扫描所有分区,再UNION结果。这类查询在业务里其实很少——几乎所有对流水表的访问接口都带时间范围,但我们还是排查了一遍所有SQL,发现有三条后台统计SQL确实没带时间条件,后来统一在应用层加了默认近30天的时间窗口,才算把隐患清完。

4.3 维护负担:vacuum不再“永远跑不完”

普通单表在2200万行的时候,autovacuum 跑一圈要好几个小时,期间还可能因为事务ID回卷问题产生告警。分区之后,每个分区只有100万行左右,autovacuum 对每个分区的处理时间降到了十几分钟级别,而且分区可以错峰清理,不会出现“一个大表卡住所有vacuum worker”的情况。

这个收益在日常看起来不起眼,但对长跑业务来说,它决定了系统能不能稳定运行一年不出幺蛾子。

5. 上线半年后,我们踩过的五个大坑

前面都是在讲怎么把分区表做对、做出收益。下面这部分是我认为最值得分享的实战排错经验,毕竟纸上谈兵谁都会,真实环境里的坑才见功力。

5.1 默认分区成了垃圾桶,导致新分区无法挂载

上线的第二个月,我们的监控脚本发现,默认分区sim_order_log_default的行数涨到了40万,而最新的业务分区只有20万。这个数据分布明显不正常。

排查过程是这样的:

  • 先查默认分区里数据的created_at范围,发现大部分数据是最近三天的,而且集中在凌晨。
  • 去看应用的日志,发现一个定时任务在跑数据对账,它对sim_order_log执行插入时,时间字段是upstream传来的字符串,但格式是2024-03-01 08:30:00这种没有时区标记的时间。
  • 我们的分区函数按UTC转换,而这个字符串被PG默认按数据库时区解析成了本地时间,于是永远差了8小时,凌晨的数据就落到了前一天的分区边界之外,全被默认分区接走了。

更麻烦的是,因为有数据卡在默认分区,我们想给下个月挂新分区时,PG报错:

ERROR: partition constraint would be violated by some row

原因很清晰:默认分区里已经有值落在新分区的范围内,挂载新分区时会冲突。当时我们花了20多分钟才把默认分区里有问题的数据挪回到正确分区,同时修掉了应用层的时间格式Bug。

提示:默认分区不是安全屋,它是“你没把事情做对的最后一道防线”。必须每天都检查默认分区行数,一旦有数据积累,先别急着挂新分区,要先把数据归位。

5.2 唯一索引必须包含分区键,PG在这个问题上一视同仁

有一次开发同事提了一个需求,要给order_no加唯一约束,结果建索引的时候直接报错:

ERROR: unique constraint on partitioned table must include all partitioning columns

这个限制很容易被忽略。原因是分区表里,PG只能保证每个分区自己内部唯一,但无法跨分区检查全局唯一。如果唯一键里没有分区键,那两个不同分区里出现相同order_no就必须靠查全表才能发现,这在分布式体系里是一个标准难题。

我们的解决方案是业务层面妥协:唯一性约束从数据库层面减少到应用层保证,给order_no加了普通索引,然后由订单号生成规则保证唯一(前缀包含日期+随机序列)。如果你必须做数据库级全局唯一,那就得把表设计成以业务键为分区键,或者用全局唯一索引方案(比如在分区父表上建基于order_no的表达式索引)——注意PG12的全局唯一索引支持是有限的,我们最终没有走这条路。

5.3 分区键用表达式后,查询条件也必须跟着用表达式

这是一个特别隐蔽的性能坑。

我们父表的分区键是timezone('UTC', created_at),PostgreSQL的分区裁剪需要优化器能从查询条件里推导出分区范围。如果业务SQL写的是:

WHERE created_at >= '2024-01-01'

那么优化器需要做等价推导,实际上在PG12里,这种推导对于表达式分区键的支持并不好,结果是查全表所有分区。

后来我们在应用层强制做了时间归一化:所有SQL条件里统一改写成:

WHERE timezone('UTC', created_at) >= '2024-01-01 00:00:00+00'

改写后,EXPLAIN 才正确显示裁剪到对应月份分区。

不同PG版本对表达式分区键的裁剪能力差别很大,PG14以后好了很多,但你在10到12版本上做生产环境,最稳妥的做法还是避免表达式分区键,直接用原始时间列比较。我们当时为了省事用了表达式,后来发现查询要处处留意,属于典型的“图省事反而更费事”。

5.4 UPDATE跨分区移动数据时,必须先开启行移动

分区表的一个冷知识:如果你更新某行数据,导致它从分区A跳跃到分区B(跨分区移动),PostgreSQL会尝试删除原行并插入到新分区。但默认情况下这个操作被禁止,并报错:

ERROR: updated partition key column does not qualify as an allowed partition key change

我们的流水表严格来说不应该出现跨分区的created_at更新,但有后台逻辑会“修正”一些脏数据的订单状态和时间戳,结果一执行就报错。

解决办法是在表上启用行移动:

ALTER TABLE sim_order_log ENABLE ROW MOVEMENT;

开启后,跨分区更新会被PG内部自动转换为DELETE+INSERT,代价是性能略低且有极小概率的失败窗口。我们只在确有必要的情况下开了,并且做了监控,避免高频跨分区更新拖垮性能。

5.5 大批量迁移后,分区统计信息不准导致执行计划恶化

数据回填完毕后,我们没有立刻跑ANALYZE,结果当天出现了一个奇怪现象:某些分区明明数据量很大,优化器却选了全表扫描,而且估算行数严重偏低。

原因很简单:数据是批量灌进去的,pg_statistic统计信息还停留在旧状态,优化器拿着过时的信息做决策,执行计划自然一塌糊涂。

解决方法是全库ANALYZE:

ANALYZE sim_order_log;

它会自动对每个分区重新统计。此后我总结经验:任何大动作(批量导入、大批量删除、DETACH再ATTACH)之后,第一件事就是ANALYZE,否则再好的分区设计也会被烂执行计划拖垮。

6. 分区表日常维护:定时任务、监控与事故防范

分区表不是建成之后就一劳永逸,它是需要“养”的。下面是我们运行稳定后的日常维护清单。

6.1 用pg_cron做三类定时任务

我们引入了pg_cron扩展来做自动维护。三个任务缺一不可:

  • 每月25号调用auto_create_log_partition,创建下下个月的分区。
  • 每月1号检查所有分区,确认没有“时间断层”(比如漏建了一个月)。
  • 每天凌晨检查默认分区行数,如果超过阈值(我们设定1万行),发告警。

pg_cron的配置很简单:

CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule( 'create-partition-monthly', '0 3 25 * *', $$SELECT auto_create_log_partition(current_date + interval '2 months')$$ );

注意一点:pg_cron默认只能调度数据库内部函数,如果你的分区创建逻辑还涉及外部通知,建议用系统 crontab 调用脚本,两边结合。

6.2 分区生命周期管理:留存期定了18个月

我们的订单流水只需要保留18个月,超过的直接DETACH然后删除。这个策略涉及两步:

-- 第一步:摘除 ALTER TABLE sim_order_log DETACH PARTITION sim_order_log_202305; -- 第二步:确认无业务访问后,DROP或归档 DROP TABLE sim_order_log_202305;

这里有个小细节:DETACH之后表仍然物理存在,数据不会被删,但已经脱离了父表管理。要彻底删除再DROP。如果有审计需求,不DROP而是转存到历史库,就把DETACH的分区直接移动到别的表空间或转成普通表,也很灵活。

提示:DETACH操作会短暂锁父表。如果你的分区特别大,DETACH会需要重写分区上的约束验证,耗时可能从毫秒到秒级。最好放在业务低谷期执行,不要高峰期裸奔。

6.3 监控SQL:每季度做一次分区健康体检

我习惯写一套SQL定期检查分区表的健康状态,这里分享几个核心的:

-- 查询每个分区的行数和大小 SELECT child.relname AS partition_name, pg_total_relation_size(child.oid) AS total_bytes, pg_stat_get_live_tuples(child.oid) AS live_tuples FROM pg_inherits JOIN pg_class parent ON pg_inherits.inhparent = parent.oid JOIN pg_class child ON pg_inherits.inhrelid = child.oid WHERE parent.relname = 'sim_order_log' ORDER BY child.relname;
-- 检查默认分区是否有数据(重点监控) SELECT count(*) FROM sim_order_log_default;
-- 检查最近3个月的分区范围是否覆盖当前时间 SELECT relname, pg_get_expr(relpartbound, oid) AS bound_expr FROM pg_class WHERE relispartition = true AND relname LIKE 'sim_order_log_%' ORDER BY relname DESC LIMIT 5;

这套SQL每季度跑一次,能发现很多潜在问题:分区大小严重不均衡、默认分区异常、漏建分区、某个分区膨胀等。配合一个简单的Shell脚本+企业微信告警,基本能做到“问题出现在别人报警之前”。

6.4 分区表函数调用顺序对写入性能的影响

最后说一个优化细节。分区表在写入时,PG需要根据分区键计算目标分区。如果分区函数是表达式,那么每次INSERT都要先做表达式计算,虽然一次计算开销很小,但高频写入下会有可感知的CPU消耗。

我们的流水表峰值写入达到每秒2000行,单次表达式计算微秒级,但累计下来大约增加了5%到8%的CPU开销。后来优化方式是在应用层直接计算出目标月份,SQL里直接指定要插入的分区表名,绕开了PG的分区路由。当然,这对代码有侵入性,我们只对最核心的两张流水表做了,收益是CPU降低,写入P99也更平稳了。

最后分享一点个人的实际体会

分区表这套方案,我到现在已经维护了一年多。最大的感受不是“快”——快是意料之中的——而是整个运维节奏变得可预期了。以前最怕夜里收到清数据任务卡死的告警,现在定时任务就是摘分区,失败了大不了手动再摘一次,风险和焦虑感完全不在一个量级。

如果你也准备做分区表改造,我的建议是:先别急着上生产,找一张业务上“查询必带时间条件、数据可定期淘汰”的表,模拟两三个月的数据量,把迁移脚本、自动建分区函数、监控SQL全部跑通,踩一遍本文提到的这些坑,然后再动核心表。PG的声明式分区本身并不复杂,复杂的是你对自己业务数据访问模式的理解。把这个想透了,分区表才会真正变成你的利器,而不是新的麻烦来源。

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

Abaqus周期性边界插件EasyPBCV 1.4安装避坑指南

简介&#xff1a;这是一款专为ABAQUS用户开发的周期性边界条件&#xff08;PBC&#xff09;自动化插件&#xff0c;面向复合材料微观力学建模领域的科研人员与高年级研究生&#xff0c;解决RVE&#xff08;代表体单元&#xff09;建模中手动施加位移-应力双连续边界条件繁琐、易…

作者头像 李华
网站建设 2026/10/21 17:44:00

Playwright替代Selenium:动态页面爬虫实战与反爬避坑指南

这两年做动态页面采集&#xff0c;Python 爬虫实战里最让我纠结的就是选型&#xff1a;Selenium 用了三年&#xff0c;写起来顺手&#xff0c;但遇到反爬和复杂等待场景越来越吃力。后来我把核心采集流程全部切到 Playwright&#xff0c;用 Playwright 替代 Selenium 之后&…

作者头像 李华
网站建设 2026/10/21 17:42:02

2026年CI/CD测试工具选型:从执行到决策的数据闭环

2026年&#xff0c;做CI/CD测试工具选型这件事&#xff0c;我最大的感触是&#xff1a;单点工具的比拼已经结束了&#xff0c;大部分项目的痛点和机会点&#xff0c;都集中在“工具链如何围绕测试数据闭环运转”这件事上。过去一年里找我聊CI/CD测试方案的朋友&#xff0c;问的…

作者头像 李华
网站建设 2026/10/21 17:41:44

AI绘图风格总飘?327种手绘风格编号化实现稳定输出

1. 当AI画图开始"飘"&#xff0c;问题到底出在哪做AI绘图相关工作的人大概都有过这种体验&#xff1a;同一段提示词&#xff0c;今天跑出来是干净利落的扁平插画&#xff0c;明天再跑就变成了厚涂油画风&#xff1b;明明想要的是"手绘感"&#xff0c;结果出…

作者头像 李华
网站建设 2026/10/21 17:41:11

三步把 AI PR 门禁接进 GitHub Actions

把 AI 接到 PR 审查上这件事&#xff0c;我去年就试过一轮。当时最大的感受不是“爽”&#xff0c;而是“乱”&#xff1a;要么是模型评论刷屏没人看&#xff0c;要么是 workflow 里一把梭跑完就散&#xff0c;完全没有起到把关的作用。后来我把整个逻辑重新拆成“拿 diff ->…

作者头像 李华
网站建设 2026/10/21 17:41:07

App渗透测试实战:SQL注入与人脸识别逻辑绕过登录任意账号

做App渗透测试这几年&#xff0c;我见过不少有意思的漏洞&#xff0c;但这个案例值得单独拿出来聊聊&#xff1a;一个带人脸识别登录的App&#xff0c;攻击链路竟然是SQL注入换数据&#xff0c;再抓住人脸识别接口的校验缺陷&#xff0c;最终成功登录任意账号。整个过程不算花哨…

作者头像 李华