做过后台开发的,基本都写过这类逻辑:
维护一个有序列表。
什么审批规则排序、菜单顺序、流程步骤、数据字典顺序…… 需求几乎长一个样:
删除一项 → 后面的序号自动减1 → 序号永远连续不中断。
听起来简单吧? 但我最近正好在代码里翻到一段真实业务逻辑(脱敏过的), 看完我沉默了十分钟。
它不是不能跑, 但你要是把它扔到高并发或者数据量稍微大一点的场景里, 早晚出事。
今天我就拿这段代码当案例, 从“能跑”一路改到“能上线扛流量”。 不吹架构,不炫技,全是真实落地过程。
public async Task RemoveOneAsync(Guid id, Guid? tenantId)
{
ApprovalRuleInfo? rule = await _repository.FindOneAsync(id);
if (rule is null)
throw new ArgumentException("审批规则不存在");
SearchConditionDTO searchCondition = new SearchConditionDTO(1, 9999, "OrderId", false);
searchCondition.AddCondition("OrderType", ((int)rule.OrderType).ToString(), RelationEnum.Equal);
ResultList rlist = await _repository.FindSomeAsync(searchCondition, tenantId, OperationTypeEnum.Write);
foreach (var item in rlist.Rows)
{
if (item.ID == id)
{
item.Remove();
}
else
{
if (item.OrderId > rule.OrderId)
item.UpdateOrder(item.OrderId - 1);
}
}
_repository.UpdateBatch(rlist.Rows);
return id;
}你说它错了吧,也没有。 功能走得通,需求也满足。 但就是有一股“写完就跑”的味道。
我当时列了一下问题:
很多系统的线上卡顿、序号错乱、删除后数据对不上, 就是这种“能跑就行”的代码堆出来的。
删除一个排序项并维护序号, 其实只有两件事:
这是一个 区间更新,不是全量更新。 但原代码最大的问题就是: 把区间操作,硬写成了全量操作。
你要是只有三五条数据还好, 一旦某个类型下有几百上千条, 它每次都把全部数据拉出来、遍历、再写回去—— 这是典型的“不知道数据库能批量操作”。
(这里本来有一张示意图,我画了个草稿,大家脑补一下:左边是查全表,右边只查OrderId大于某个值的)
原代码里这个条件,看得我血压有点高:
SearchConditionDTO(1, 9999, "OrderId", false);9999??
哪怕只有20条数据,它也敢查9999条回来。
而且是在内存里再判断 if (item.OrderId > rule.OrderId)。
我就问一句: 为什么不在数据库里就过滤掉?
改成这样:
searchCondition.AddCondition("OrderId", rule.OrderId.ToString(), RelationEnum.GreaterThan);就这么一行改动:
这个优化,性价比最高。 不换架构、不改SQL(对,他们用的是自己封装的查询条件), 但收益肉眼可见。
原代码的循环长这样:
foreach(...) {
if(是删除项) 删除
else 更新序号
}看着好像也没啥, 但你要是出过一次bug就知道了: 到底是删错了,还是更新错了? 日志打出来都分不清。
我的习惯是 职责分离:
// 1. 先删
await _repository.DeleteAsync(rule);
// 2. 再批量更新后面的序号
await _repository.UpdateBatchOrderAsync(rule.OrderType, rule.OrderId, tenantId);一眼就知道在干什么。 这叫不叫“单一职责”我不在乎, 但下一个人接手的时候不会骂我。
(这里本来有张对比图,我懒得画了,就是左边一堆if else,右边两行清晰代码)
这是我最想说的。
很多人写业务代码,从来没想过可以不用循环。
原代码: 查N条 → 遍历N条 → 更新N条 → 复杂度 O(N)
其实一条SQL就够了:
UPDATE ApprovalRuleInfo
SET OrderId = OrderId - 1
WHERE OrderType = @OrderType
AND OrderId > @DeletedOrderId
AND TenantId = @TenantId就这么简单。 没有内存消耗,没有网络来回,数据库自己就搞定了。 不管你是10条还是10000条,时间基本恒定。
我第一次改成这种方式的时候, 同事问:“你确定不需要foreach?” 我直接让他跑了次性能对比,他不说话了。
原代码完全没有事务。
这意味着什么? 高并发下,两个人同时删同类型的规则:
这些事情我线上都见过。 有一次半夜被叫起来,就是因为没加事务,数据乱成一锅粥。
正确姿势:
using var transaction = await _repository.BeginTransactionAsync();
try
{
await _repository.DeleteAsync(rule);
await _repository.UpdateBatchOrderAsync(rule.OrderType, rule.OrderId, tenantId);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}这句话我希望你记住: 线上系统,事务不是“可选”,是“必须”。
改都改了,干脆把那些小毛病一起修了:
ArgumentException 改成明确的 EntityNotFoundExceptionreturn id 改成 Task这些不复杂,但能少挨很多骂。
///
/// 删除审批规则,并自动维护后续序号/// 改了好几版,这个算能见人的///
public async Task RemoveOneAsync(Guid id, Guid? tenantId)
{
var rule = await _repository.FindOneAsync(id, tenantId)
?? throw new EntityNotFoundException(nameof(ApprovalRuleInfo), id);
using var transaction = await _repository.BeginTransactionAsync();
try
{
await _repository.DeleteAsync(rule);
await _repository.UpdateOrderAfterDeleteAsync(
rule.OrderType,
rule.OrderId,
tenantId);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}public async Task UpdateOrderAfterDeleteAsync(OrderTypeEnum orderType, int deletedOrderId, Guid? tenantId)
{
var sql = @"
UPDATE ApprovalRuleInfo
SET OrderId = OrderId - 1
WHERE OrderType = @OrderType
AND OrderId > @DeletedOrderId
AND TenantId = @TenantId";
var param = new
{
OrderType = (int)orderType,
DeletedOrderId = deletedOrderId,
TenantId = tenantId
};
await ExecuteSqlAsync(sql, param);
}public class EntityNotFoundException : Exception
{
public EntityNotFoundException(string entityName, object key)
: base($"[{entityName}] 主键 {key} 不存在") { }
}代码改完以后,我跟产品同学聊了一次。
我说: “序号真的必须连续吗?”
他愣了一下。
我们绝大多数场景里,OrderId 只是为了排序。
1,2,3,5,6 排序结果完全正确啊。
这个思路一出来, 删除操作直接从 O(N) 变成 O(1)。
这是我比较得意的地方: 最好的优化,有时候不是改代码,而是重新理解需求。
我简单列了个对比,不搞花哨表格:
方面 | 改之前 | 改之后 |
|---|---|---|
查询方式 | 全量拉回来 | 只查需要的区间 |
更新方式 | 内存里一条条改 | 一条SQL批量更新 |
性能 | 数据一多就慢 | 固定时间,几乎不变 |
并发安全 | 没事务,各种乱 | 事务保底,稳 |
代码可读性 | 得看好几遍 | 一眼就懂 |
内存占用 | 高 | 极低 |
谁愿意维护 | 没人想碰 | 随便谁都能接 |
这段代码看起来很小, 但把它改好的过程,其实能看出一个人在哪一层思考:
我越来越觉得, 高级工程师不是写得出一堆复杂代码, 而是能把复杂的东西变得简单、安全、快。
(这里本来有张四层图,我懒得配了,你们脑补一下金字塔就行)
这段“删规则调顺序”的代码, 看起来只是日常业务中再普通不过的一段。 但真把它改好、改稳、改到能扛并发, 它就能区分: 能跑 vs 能上线。
优化的从来不是一行代码, 而是系统的稳定性、并发能力、以及后面人的心情。
优化没有尽头,但思路有迹可循。
👋 我是 [云中小生],一个经常在后端踩坑又爬出来的程序员。 不定期分享:后端实战、性能优化、真实踩坑、架构思考。 不整虚的,只写能落地的。
(点击关注,修炼不迷路👇)
▌转载请注明出处,渡人渡己
🌟 感谢道友结缘! 若本文助您突破修为瓶颈,不妨【转发功德】,让更多道友共参.NET天道玄机。修真之路漫漫,我们以代码为符,共绘仙途!