news 2026/10/15 6:56:33

SQL Server与MySQL字符串分割函数实现:从原理到高性能实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL Server与MySQL字符串分割函数实现:从原理到高性能实践

1. 为什么我要自己写一个字符串分割函数

但凡做过几年数据开发的人,大概率都遇到过这种场景:前端传过来一串用逗号拼起来的ID,比如"1001,1002,1003",需要在数据库里把它拆成多行做关联查询;或者从上游系统同步过来一批标签数据,用竖线分隔,要拆开做统计分析。这时候你打开数据库一看,SQL Server 有STRING_SPLIT,但那玩意儿在早期版本里根本不支持输出序号,而且对中文逗号、多字符分隔符的处理也不够灵活;MySQL 倒是有SUBSTRING_INDEX,可它只能取第N个,想拆成结果集还得配合数字辅助表,写起来又臭又长。

我在实际项目里被这个问题反复折磨过好几次。最典型的一次是做一个报表系统,业务方要求支持多选筛选,前端把选中的部门编码用逗号拼成一个字符串丢过来,后端直接把这个字符串塞进IN条件里。结果就是WHERE dept_code IN ('1001,1002,1003')这种写法,数据库把它当成一个完整的字符串去匹配,永远查不出数据。当时年轻,第一反应是在应用层拆好再拼SQL,但这样又带来了SQL注入的风险和性能问题。后来痛定思痛,决定在数据库层面自己实现一套通用的字符串分割函数,SQL Server 和 MySQL 各写一份,彻底解决这类问题。

这篇文章就是把我这些年写过的、改过的、踩过坑的分割函数实现整理出来。不管你是刚入行的数据库新手,还是做了多年CRUD想补补底层功底的老手,都能从里面找到可以直接抄作业的代码。我会把每个实现的设计思路、参数选择、性能考量、以及实际使用中遇到的坑都讲清楚,让你不仅拿到代码,还知道为什么这么写。

2. 字符串分割的核心设计思路拆解

2.1 到底该用函数还是存储过程

这是第一个要做的决策。函数和存储过程在数据库里是两种不同的存在,选错了后面会很难受。

标量函数(Scalar Function)返回单个值,表值函数(Table-Valued Function)返回一张表。字符串分割的本质是"一行变多行",所以表值函数是天然的选择。存储过程虽然也能返回结果集,但它不能用在SELECT ... FROM里面,也没法跟其他表做 JOIN,灵活性差了一大截。我早期图省事写过存储过程版本,结果业务方要求"把分割结果跟订单表关联一下",当场就傻眼了,只能推倒重来。

所以结论很明确:优先用表值函数。SQL Server 里叫Table-Valued Function,MySQL 里没有真正的表值函数,但可以用存储过程配合临时表来模拟,或者干脆用递归CTE(公用表表达式)来实现。下面我会分别讲。

2.2 分隔符的处理策略

分隔符这东西看着简单,实际坑很多。单字符分隔符(比如逗号、竖线)最好处理,多字符分隔符(比如"||"、"::")就麻烦一些。还有一个容易被忽略的点:连续分隔符。比如"1001,,1003",中间有两个逗号,你是当成一个空字符串处理,还是直接跳过?这个取决于业务需求,但函数最好提供一个开关参数来控制。

我的做法是默认跳过空字符串,因为实际业务里连续分隔符往往意味着数据质量问题,返回空行反而会干扰后续查询。但如果业务明确需要保留空位(比如解析CSV时列位置有意义),那就得提供选项。

2.3 序号输出的必要性

很多人写分割函数只返回拆分后的值,不返回序号。这在简单场景下够用,但一旦涉及到"按原始顺序排列"或者"取第N个元素",没有序号就抓瞎了。比如前端传过来"张三,李四,王五",你想知道"李四"是第几个,没有序号列就只能靠字符串查找,效率极低。

所以我的函数设计里,序号列是标配。SQL Server 的STRING_SPLIT在2016版本刚出来时就没有序号,直到2022版本才加了ordinal参数,这就是血的教训。自己写函数,一定要把序号带上。

2.4 性能与内存的权衡

字符串分割本质上是一个循环或者递归的过程。循环次数等于分隔符出现的次数加一。对于短字符串(几百个字符),怎么实现都无所谓;但如果字符串长度上万,分隔符上千个,性能差异就出来了。

SQL Server 里常见的实现方式有三种:WHILE循环配合CHARINDEX、递归CTE、以及利用XML或JSON解析。WHILE循环最直观但性能一般;递归CTE写起来优雅,但默认递归深度限制是100,超过就会报错,需要加OPTION (MAXRECURSION 0);XML 方式性能最好,但代码可读性差,而且对特殊字符需要转义。

MySQL 8.0 之前没有递归CTE,只能用数字辅助表或者存储过程循环。8.0 之后可以用递归CTE,写法跟 SQL Server 类似。我下面会给出两种数据库各自最稳妥的实现。

3. SQL Server 表值函数完整实现

3.1 基础版本:WHILE循环实现

先看最直观的版本,适合理解原理,也适合在低版本 SQL Server 上使用。

CREATE FUNCTION dbo.fn_SplitString ( @InputString NVARCHAR(MAX), @Delimiter NVARCHAR(10) ) RETURNS @Result TABLE ( Id INT IDENTITY(1,1), Value NVARCHAR(MAX) ) AS BEGIN IF @InputString IS NULL OR LEN(@InputString) = 0 RETURN; DECLARE @StartIndex INT = 1; DECLARE @EndIndex INT; DECLARE @Item NVARCHAR(MAX); SET @InputString = @InputString + @Delimiter; WHILE CHARINDEX(@Delimiter, @InputString, @StartIndex) > 0 BEGIN SET @EndIndex = CHARINDEX(@Delimiter, @InputString, @StartIndex); SET @Item = SUBSTRING(@InputString, @StartIndex, @EndIndex - @StartIndex); IF LEN(@Item) > 0 INSERT INTO @Result (Value) VALUES (@Item); SET @StartIndex = @EndIndex + LEN(@Delimiter); END RETURN; END

这个版本有几个关键点值得说。第一,我在输入字符串末尾追加了一个分隔符,这样循环条件统一,不用在循环外单独处理最后一段。第二,@Delimiter的长度用LEN而不是固定1,这样支持多字符分隔符。第三,IF LEN(@Item) > 0这行就是控制是否跳过空字符串的开关,如果你需要保留空位,把这行去掉即可。

调用方式:

SELECT * FROM dbo.fn_SplitString('1001,1002,1003', ',');

结果会返回三行,Id 分别是1、2、3,Value 分别是1001、1002、1003。

3.2 进阶版本:支持序号和空值保留

基础版本够用,但不够灵活。实际项目里我更需要一个带选项参数的版本。

CREATE FUNCTION dbo.fn_SplitStringEx ( @InputString NVARCHAR(MAX), @Delimiter NVARCHAR(10), @KeepEmpty BIT = 0 ) RETURNS @Result TABLE ( Ordinal INT, Value NVARCHAR(MAX) ) AS BEGIN IF @InputString IS NULL OR LEN(@InputString) = 0 RETURN; DECLARE @StartIndex INT = 1; DECLARE @EndIndex INT; DECLARE @Item NVARCHAR(MAX); DECLARE @Ordinal INT = 0; SET @InputString = @InputString + @Delimiter; WHILE CHARINDEX(@Delimiter, @InputString, @StartIndex) > 0 BEGIN SET @EndIndex = CHARINDEX(@Delimiter, @InputString, @StartIndex); SET @Item = SUBSTRING(@InputString, @StartIndex, @EndIndex - @StartIndex); SET @Ordinal = @Ordinal + 1; IF @KeepEmpty = 1 OR LEN(@Item) > 0 INSERT INTO @Result (Ordinal, Value) VALUES (@Ordinal, @Item); SET @StartIndex = @EndIndex + LEN(@Delimiter); END RETURN; END

这个版本把序号显式地作为一列返回,而且序号是在插入前就递增的,所以即使跳过了空字符串,序号也不会断。这一点很重要,因为有时候你需要知道某个值在原始字符串里的位置。

3.3 高性能版本:XML解析法

当字符串很长、分割次数很多时,WHILE循环的性能会成为瓶颈。这时候可以用 XML 方式,把字符串转成 XML 节点列表,一次性解析。

CREATE FUNCTION dbo.fn_SplitStringXml ( @InputString NVARCHAR(MAX), @Delimiter NVARCHAR(10) ) RETURNS TABLE AS RETURN ( SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS Ordinal, LTRIM(RTRIM(T.c.value('.', 'NVARCHAR(MAX)'))) AS Value FROM ( SELECT CAST('<x>' + REPLACE( REPLACE( REPLACE(@InputString, '&', '&amp;'), '<', '&lt;'), @Delimiter, '</x><x>') + '</x>' AS XML) AS XmlData ) AS T CROSS APPLY XmlData.nodes('/x') AS T2(c) WHERE LTRIM(RTRIM(T.c.value('.', 'NVARCHAR(MAX)'))) <> '' );

这个版本是内联表值函数(Inline TVF),性能比多语句表值函数好很多,因为查询优化器可以把它展开优化。注意里面的REPLACE嵌套,是为了处理 XML 特殊字符。&必须最先替换,否则会把后面替换产生的&lt;里的&又转义一遍,导致结果错误。这个顺序坑了我半个小时。

注意:XML 方式对分隔符里包含 XML 特殊字符的情况支持不好,如果你的分隔符是"<"或者"&",建议还是用 WHILE 版本。

3.4 三种实现方式的对比

实现方式适用场景优点缺点
WHILE循环通用场景、低版本逻辑直观、易调试长字符串性能差
带选项WHILE需要控制空值灵活、序号可控同上
XML解析高性能要求速度快、内联可优化特殊字符需转义

我个人的选择是:日常用带选项的 WHILE 版本,数据量大的ETL场景用 XML 版本。两者可以共存,按需调用。

4. MySQL 存储过程与递归CTE实现

4.1 MySQL 8.0 递归CTE方案

MySQL 8.0 引入了递归CTE,这让字符串分割变得优雅很多。核心思路是:每次递归找到第一个分隔符,切出前面的部分,然后把剩下的部分传给下一轮递归。

WITH RECURSIVE split_cte AS ( SELECT 1 AS ordinal, SUBSTRING_INDEX('1001,1002,1003', ',', 1) AS value, SUBSTRING('1001,1002,1003', LENGTH(SUBSTRING_INDEX('1001,1002,1003', ',', 1)) + 2) AS remainder UNION ALL SELECT ordinal + 1, SUBSTRING_INDEX(remainder, ',', 1), IF(LOCATE(',', remainder) > 0, SUBSTRING(remainder, LENGTH(SUBSTRING_INDEX(remainder, ',', 1)) + 2), '') FROM split_cte WHERE remainder <> '' ) SELECT ordinal, value FROM split_cte;

这里有几个细节。SUBSTRING_INDEX(str, ',', 1)取第一个分隔符之前的部分。LENGTH(...) + 2里的+2是因为分隔符本身占一个字符,再加1跳过它。如果分隔符是多字符,这个偏移量要相应调整。remainder <> ''是递归终止条件。

这个写法可以直接用在查询里,不需要创建存储过程。但如果你要在多个地方复用,还是封装成存储过程更好。

4.2 MySQL 存储过程版本

MySQL 的存储过程可以返回结果集,虽然不能像表值函数那样用在 FROM 子句里,但配合临时表也能达到类似效果。

DELIMITER $$ CREATE PROCEDURE sp_SplitString( IN p_input VARCHAR(65535), IN p_delimiter VARCHAR(10) ) BEGIN DECLARE v_start INT DEFAULT 1; DECLARE v_end INT; DECLARE v_item VARCHAR(1000); DECLARE v_ordinal INT DEFAULT 0; DROP TEMPORARY TABLE IF EXISTS tmp_split_result; CREATE TEMPORARY TABLE tmp_split_result ( ordinal INT, value VARCHAR(1000) ); IF p_input IS NULL OR LENGTH(p_input) = 0 THEN SELECT * FROM tmp_split_result; ELSE SET p_input = CONCAT(p_input, p_delimiter); WHILE LOCATE(p_delimiter, p_input, v_start) > 0 DO SET v_end = LOCATE(p_delimiter, p_input, v_start); SET v_item = SUBSTRING(p_input, v_start, v_end - v_start); SET v_ordinal = v_ordinal + 1; IF LENGTH(v_item) > 0 THEN INSERT INTO tmp_split_result VALUES (v_ordinal, v_item); END IF; SET v_start = v_end + LENGTH(p_delimiter); END WHILE; SELECT * FROM tmp_split_result; END IF; END$$ DELIMITER ;

调用方式:

CALL sp_SplitString('1001,1002,1003', ',');

这个存储过程每次调用都会重建临时表,所以不用担心数据残留。但要注意,临时表是会话级别的,同一个会话里多次调用会互相覆盖,如果嵌套调用会有问题。实际使用中我建议把结果先落到普通表或者用递归CTE替代。

4.3 MySQL 数字辅助表方案

在 MySQL 8.0 之前,没有递归CTE,最常用的方案是数字辅助表。思路是预先准备一张包含连续整数的表,然后用SUBSTRING_INDEX配合序号来切分。

-- 创建数字辅助表 CREATE TABLE numbers (n INT PRIMARY KEY); INSERT INTO numbers (n) SELECT a.N + b.N * 10 + c.N * 100 + 1 FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a CROSS JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b CROSS JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c ORDER BY 1;

这张表包含1到1000的连续整数,足够大多数场景使用。然后分割查询这样写:

SELECT n AS ordinal, SUBSTRING_INDEX(SUBSTRING_INDEX('1001,1002,1003', ',', n), ',', -1) AS value FROM numbers WHERE n <= LENGTH('1001,1002,1003') - LENGTH(REPLACE('1001,1002,1003', ',', '')) + 1;

这个写法的精髓在于SUBSTRING_INDEX嵌套。内层取前N段,外层取最后一段,就得到了第N个元素。WHERE条件计算分隔符数量加一,就是总段数。这个方案性能很好,因为不需要循环,纯集合操作。

提示:数字辅助表的行数要大于最大可能的分割段数。如果字符串可能包含上千个分隔符,辅助表就要建到上千行。我一般建到10000行,用交叉连接很容易生成。

4.4 两种数据库实现的差异对照

对比项SQL ServerMySQL
函数类型表值函数,可用于FROM存储过程或CTE
递归支持递归CTE,默认深度1008.0+支持递归CTE
分隔符处理CHARINDEX定位LOCATE定位
序号输出IDENTITY或ROW_NUMBER变量递增或ROW_NUMBER
性能优化XML解析、内联TVF数字辅助表

5. 实操过程中的关键细节与避坑经验

5.1 分隔符长度导致的偏移量错误

这是最容易踩的坑。很多人在计算下一个起始位置时,习惯性地写@EndIndex + 1,默认分隔符只有一个字符。但如果分隔符是"||",加1就错了,会漏掉一个字符。正确的做法是@EndIndex + LEN(@Delimiter)。我在一个项目里用"::"做分隔符,就是因为这个加1的问题,导致所有偶数位置的数据都少了一个字符,排查了半天才发现。

5.2 空字符串与NULL的区别

LEN('')返回0,LEN(NULL)返回NULL。在IF判断里,NULL会导致条件为假,所以如果输入是NULL,函数会直接返回空结果集,这是符合预期的。但如果你在函数内部对NULL做了拼接操作,比如SET @InputString = @InputString + @Delimiter,结果还是NULL,循环条件CHARINDEX返回NULL,循环体不会执行,最终返回空结果集。这个行为是安全的,但最好在函数开头显式判断NULL并返回。

5.3 递归CTE的深度限制

SQL Server 的递归CTE默认最大递归深度是100。如果你的字符串有超过100个分隔符,就会报错The statement terminated. The maximum recursion 100 has been exhausted。解决办法是在查询末尾加OPTION (MAXRECURSION 0),0表示不限制。但要注意,不限制递归深度有栈溢出的风险,对于超长字符串,还是用WHILE循环或者数字辅助表更稳妥。

5.4 临时表在存储过程中的并发问题

MySQL 存储过程里用临时表存分割结果,在同一个会话里多次调用会互相干扰。比如你在一个存储过程里调用了分割过程,然后又调用另一个也用了同名临时表的过程,数据就乱了。我的做法是临时表名加上随机后缀,或者干脆用递归CTE替代存储过程。如果非要用临时表,记得在过程开头DROP TEMPORARY TABLE IF EXISTS,过程结束时不删,让会话结束时自动清理。

5.5 性能实测数据参考

我在一台测试机上做过对比,输入字符串包含1000个逗号分隔的数字,SQL Server 三种实现方式的耗时大致如下:

实现方式耗时(毫秒)备注
WHILE循环约120稳定但偏慢
XML解析约15最快
递归CTE约80受递归深度影响

MySQL 方面,数字辅助表方案约20毫秒,存储过程循环约150毫秒,递归CTE约60毫秒。数据仅供参考,实际性能跟硬件和字符串特征有关,但趋势是一致的:集合操作优于循环,内联优于多语句。

6. 常见问题排查与速查表

6.1 分割结果少了最后一段

这是最经典的问题。原因通常是循环终止条件写成了CHARINDEX(@Delimiter, @InputString, @StartIndex) > 0,但没有在输入字符串末尾补分隔符。最后一段后面没有分隔符,CHARINDEX返回0,循环退出,最后一段就丢了。解决办法就是在循环前执行SET @InputString = @InputString + @Delimiter。

6.2 中文分隔符乱码

如果分隔符是中文逗号",",在NVARCHAR类型下没问题,但如果输入字符串是VARCHAR类型,而数据库排序规则不是中文,就可能出现乱码。建议函数参数统一用NVARCHAR,调用时字符串前加N前缀,比如N'1001,1002'。

6.3 分割后需要去重

有时候业务要求分割后去重。这个不要在函数里做,因为函数应该保持单一职责。在调用函数后加DISTINCT或者GROUP BY即可。比如:

SELECT DISTINCT Value FROM dbo.fn_SplitString('a,b,a,c', ',');

6.4 常见问题速查表

问题现象可能原因解决方法
结果少最后一段未补末尾分隔符循环前拼接分隔符
多字符分隔符切错偏移量加1而非加长度用LEN(@Delimiter)
递归报错深度超限默认100层限制加OPTION (MAXRECURSION 0)
中文乱码字符集不匹配统一用NVARCHAR/N前缀
空字符串干扰未过滤空值加LEN判断或KeepEmpty参数
临时表数据串会话内多次调用加随机后缀或改用CTE

6.5 一个容易被忽略的边界情况

如果输入字符串本身就是分隔符,比如",",按照我的实现,会返回一个空结果集(因为跳过了空字符串)。但有些业务期望返回两个空字符串。这个没有对错,取决于约定。我的建议是在函数文档里写清楚行为,让调用方知道。如果确实需要保留,把KeepEmpty参数设为1即可。

7. 实际项目中的应用场景扩展

7.1 多选筛选条件的动态SQL

回到开头提到的报表筛选场景。有了分割函数,可以这样写:

SELECT o.* FROM orders o INNER JOIN dbo.fn_SplitString(@DeptCodes, ',') d ON o.dept_code = d.Value;

这样就把一个字符串参数变成了一个可以JOIN的结果集,既避免了SQL注入,又利用了索引。比在应用层拼SQL优雅得多。

7.2 标签数据的行转列与列转行

用户标签通常存成"tag1|tag2|tag3"的形式。用分割函数可以轻松拆成多行,然后跟标签维度表关联,做标签统计。反过来,用STRING_AGG(SQL Server)或GROUP_CONCAT(MySQL)可以把多行合并回字符串。这两个函数配合使用,行转列和列转行就都齐了。

7.3 批量导入时的数据清洗

从外部系统导入数据时,经常遇到一个字段里塞了多个值的情况。比如地址字段"省,市,区"。用分割函数拆开后,可以分别校验省市区是否合法,把不合法的数据挑出来。这比在应用层用代码拆效率高得多,尤其是数据量大的时候。

7.4 与JSON函数的配合

现在很多数据库都支持JSON类型。如果上游传过来的是JSON数组,比如["1001","1002"],其实不需要分割函数,直接用JSON解析函数就行。但如果传过来的是"1001,1002"这种字符串,还是得用分割函数。我一般会在ETL层做个判断,能JSON就JSON,不能就分割。

8. 我个人的一些使用体会

写了这么多年分割函数,最大的体会是:不要过度设计。我见过有人写了一个支持正则表达式分隔符、支持转义字符、支持嵌套引号的分割函数,代码上千行,结果项目里从来没用过那些高级功能,反而因为逻辑复杂出了好几个bug。对于90%的场景,一个支持多字符分隔符、能跳过空值、带序号的简单函数就足够了。

另一个体会是,函数命名要规范。我习惯用fn_前缀表示函数,sp_前缀表示存储过程。虽然 SQL Server 里sp_前缀是系统存储过程保留的,自定义的用sp_会有轻微性能损失,但团队里统一了命名规范,维护起来方便很多。如果在意性能,可以用usp_或者proc_前缀。

最后说一个实际踩过的坑。有一次我在一个高并发的系统里用了多语句表值函数做分割,结果发现CPU占用很高。排查后发现是表值函数里的INSERT INTO @Result操作在每次调用时都会分配表变量,数据量大时内存压力很大。后来改成内联表值函数(XML版本),问题就解决了。所以如果你的分割函数会被频繁调用,优先考虑内联表值函数,而不是多语句的。

对于MySQL,我现在的首选是递归CTE,代码短、可读性好、性能也不错。只有在MySQL 5.7及以下版本才用数字辅助表。存储过程版本我基本已经弃用了,因为临时表的并发问题太烦人,而且调用方式不如CTE灵活。

如果你正在做数据仓库或者ETL开发,建议把分割函数作为基础工具库的一部分,提前建好。等到用的时候直接调用,比临时写要省事得多。而且这种函数一旦写好,基本不用改,属于一次投入长期受益的典型。

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

SpringBoot+Vue3图书管理系统毕设全流程复盘

每年的毕设答辩季&#xff0c;图书管理系统总会以极高的频率出现在答辩名单里。这个题目太经典了&#xff0c;以至于很多同学的第一反应是“会不会太简单”。但真正动手之后你会发现&#xff0c;它简单的是业务规则&#xff0c;不简单的是一整套前后端分离架构里那些“看起来会…

作者头像 李华
网站建设 2026/10/15 6:56:08

Verso 浏览器上手指南:用 Servo 引擎写一个自己的 Rust 浏览器

Verso 浏览器上手指南&#xff1a;用 Servo 引擎写一个自己的 Rust 浏览器 【免费下载链接】verso Mirror of https://gitlab.com/verso-browser/verso 项目地址: https://gitcode.com/GitHub_Trending/vers/verso 如果你想在本地动手跑一个不依赖 Chromium、改用 Servo…

作者头像 李华
网站建设 2026/10/15 6:55:27

SD-SDI BT656 720×576i 数字视频流编码与FPGA实现

简介&#xff1a;这份资料面向FPGA视频开发方向的工程师与课程设计学生&#xff0c;聚焦SD-SDI标准下720576i&#xff08;PAL制&#xff09;数字视频流的BT656编码实现。内容系统梳理了576i行数据格式、各字段组成、帧与场结构&#xff0c;明确消隐区与有效显示的起止行数、奇偶…

作者头像 李华
网站建设 2026/10/15 6:55:26

airllm轻量推理框架:让大模型在消费级显卡上实现本地化部署

模型权重刚下载完那一刻&#xff0c;我差点以为这台机器要原地起飞。一个十几GB的开源基座模型&#xff0c;光原始权重就占掉了大半内存&#xff0c;我的显卡显存只有那么点&#xff0c;加载到一半直接报Out of Memory。这是两年前我刚开始碰大模型本地化部署时最熟悉的场景。后…

作者头像 李华
网站建设 2026/10/15 6:54:28

JDK 11升级实战:从ZGC到模块化,迁移踩坑与核心特性全解析

JDK 11 发布到现在已经不算新了&#xff0c;但每隔一段时间我都会遇到有人问&#xff1a;项目还在 JDK 8&#xff0c;要不要升 11&#xff1f;升了到底能带来什么&#xff1f;说实话&#xff0c;JDK 11 这个版本在 Java 发展史上是一个非常特殊的节点&#xff0c;它既是 Oracle…

作者头像 李华
网站建设 2026/10/15 6:53:45

思科设备企业网络搭建实战:VLAN划分、单臂路由与NAT配置详解

最近刚完成一个挺典型的项目&#xff1a;用思科设备给一家几十人的公司搭一套完整的网络环境&#xff0c;并且要输出一张能直接看懂的网络简图。这个任务听起来不复杂&#xff0c;但真正上手你会发现&#xff0c;它几乎把企业网络里最常用的基础功能都串了一遍——VLAN划分、VL…

作者头像 李华