<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Best-Practice on Coding with Denglei</title>
    <link>https://blog.denglei.me/tags/best-practice/</link>
    <description>Recent content in Best-Practice on Coding with Denglei</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Thu, 25 Jun 2026 23:06:22 +0800</lastBuildDate>
    <atom:link href="https://blog.denglei.me/tags/best-practice/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>.NET 中 .Result 避坑指南：不同框架下的死锁与线程池饥饿</title>
      <link>https://blog.denglei.me/posts/dotnet-result-deadlock-threadpool-starvation/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/dotnet-result-deadlock-threadpool-starvation/</guid>
      <description>&lt;p&gt;这篇只讲一个知识点：在 .NET 代码里用 &lt;code&gt;.Result&lt;/code&gt;（或 &lt;code&gt;GetAwaiter().GetResult()&lt;/code&gt;）同步阻塞异步任务，为什么在不同框架下会触发不同类型的事故。&lt;/p&gt;&#xA;&lt;h2 id=&#34;问题背景&#34;&gt;问题背景&lt;/h2&gt;&#xA;&lt;p&gt;同样一行代码，在两个系统里出现了完全不同的故障：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;老系统（ASP.NET MVC 5）请求直接卡死，不返回&lt;/li&gt;&#xA;&lt;li&gt;新系统（ASP.NET Core）不是直接死锁，而是高峰期吞吐突然掉到很低，请求排队超时&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两边都有这段写法：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetDataAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetDataAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Delay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;原理同一个坑两种后果&#34;&gt;原理：同一个坑，两种后果&lt;/h2&gt;&#xA;&lt;h3 id=&#34;场景-1aspnet-classic--winforms--wpf有-synchronizationcontext&#34;&gt;场景 1：ASP.NET Classic / WinForms / WPF（有 SynchronizationContext）&lt;/h3&gt;&#xA;&lt;p&gt;这类框架默认要求 continuation 回到原上下文（UI 线程或请求上下文）。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;.Result&lt;/code&gt; 先把当前线程阻塞住，Task 完成后 continuation 又想回到这条线程，结果互相等待：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;当前线程在 &lt;code&gt;.Result&lt;/code&gt; 处阻塞&lt;/li&gt;&#xA;&lt;li&gt;continuation 需要回到当前线程继续执行&lt;/li&gt;&#xA;&lt;li&gt;当前线程被阻塞，continuation 进不来&lt;/li&gt;&#xA;&lt;li&gt;死锁&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;所以你会看到&amp;quot;请求一直转圈&amp;quot;或&amp;quot;界面完全卡死&amp;quot;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;场景-2aspnet-core默认无-synchronizationcontext&#34;&gt;场景 2：ASP.NET Core（默认无 SynchronizationContext）&lt;/h3&gt;&#xA;&lt;p&gt;在默认配置下，ASP.NET Core 没有传统的请求级 &lt;code&gt;SynchronizationContext&lt;/code&gt;，所以通常不会触发上面的经典互锁。&lt;/p&gt;&#xA;&lt;p&gt;它会把线程池工作线程同步阻塞住。并发一上来，越来越多线程被卡在 &lt;code&gt;.Result&lt;/code&gt;，线程池来不及补充，新请求拿不到线程，就出现线程饥饿：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;CPU 不一定高&lt;/li&gt;&#xA;&lt;li&gt;数据库不一定慢&lt;/li&gt;&#xA;&lt;li&gt;但接口耗时和超时数暴涨&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这就是&amp;quot;看起来不像死锁，但系统几乎不可用&amp;quot;的典型表现。&lt;/p&gt;&#xA;&lt;h2 id=&#34;最小对照示例&#34;&gt;最小对照示例&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;sealed&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;DemoService&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ❌ 错误：同步包装异步&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetNumber&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetNumberAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ✅ 正确：异步到底&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GetNumberAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Delay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;42&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;如何避坑只保留最关键三条&#34;&gt;如何避坑（只保留最关键三条）&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;不要在任何业务调用链上使用 &lt;code&gt;.Result&lt;/code&gt; / &lt;code&gt;.Wait()&lt;/code&gt; / &lt;code&gt;GetAwaiter().GetResult()&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;API、Service、Repository 全链路改成 &lt;code&gt;async&lt;/code&gt;，不要做&amp;quot;同步方法包异步&amp;quot;。&lt;/li&gt;&#xA;&lt;li&gt;如果历史包袱必须保留同步签名，就让边界层同步，内部仍然异步，避免层层阻塞传染。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;一句结论&#34;&gt;一句结论&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;.Result&lt;/code&gt; 在老框架里更容易直接死锁，在 ASP.NET Core 里更容易演化成线程池饥饿；表现不同，本质相同，都是&amp;quot;阻塞等待异步&amp;quot;导致的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>ASP.NET Core 外部依赖调用治理实战：HttpClientFactory、Polly 与幂等边界</title>
      <link>https://blog.denglei.me/posts/aspnet-core-httpclientfactory-polly-resilience/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/aspnet-core-httpclientfactory-polly-resilience/</guid>
      <description>&lt;p&gt;订单服务最容易出现的稳定性问题，不是业务代码写错，而是下游支付、库存、短信网关一抖，整个接口成功率跟着雪崩。看起来只是一次超时，实际上会引发重试风暴、线程池占满、数据库回写积压。今天这篇就只做一件事：把外部依赖调用链路收敛到可控、可观测、可恢复的状态。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-问题背景服务没挂为什么成功率先掉&#34;&gt;1. 问题背景：服务没挂，为什么成功率先掉&lt;/h2&gt;&#xA;&lt;p&gt;线上经常出现这种现象：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;API 进程还活着，CPU 占用也不高，可能只有40%（举个例子，非真实数据）。&lt;/li&gt;&#xA;&lt;li&gt;请求延迟从 200ms 拉到 6s。&lt;/li&gt;&#xA;&lt;li&gt;失败率增多。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这类故障通常不是单点问题，大概就是遇到下面这些因素了：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;HttpClient 使用方式不当。&lt;/li&gt;&#xA;&lt;li&gt;超时和重试没有预算控制，单次请求被放大成多次慢调用。&lt;/li&gt;&#xA;&lt;li&gt;对写操作直接重试却没有幂等约束，最终引发重复扣款或重复下单。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;对于有多年经验的开发者来说，这不是 API 会不会调的问题，而是如何在高并发场景下把失败控制在局部，不让故障扩散到整条链路。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-原理解析连接超时重试和幂等为什么必须一起设计&#34;&gt;2. 原理解析：连接、超时、重试和幂等为什么必须一起设计&lt;/h2&gt;&#xA;&lt;h3 id=&#34;21-连接管理决定了你能扛多久&#34;&gt;2.1 连接管理决定了你能扛多久&lt;/h3&gt;&#xA;&lt;p&gt;每次请求都新建 HttpClient，会导致连接池无法稳定复用，遇到峰值时容易把端口和连接资源打爆。更隐蔽的问题是连接存活太久导致 DNS 变更不生效，流量继续打到旧节点。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;IHttpClientFactory&lt;/code&gt; 的价值不是语法糖，而是把连接池生命周期交给 &lt;code&gt;SocketsHttpHandler&lt;/code&gt; 管理。常见配置里至少要有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;PooledConnectionLifetime&lt;/code&gt;：定期轮换连接，避免长期粘住旧地址。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;MaxConnectionsPerServer&lt;/code&gt;：控制单机并发上限，避免瞬时过载。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;22-超时预算必须先于重试策略&#34;&gt;2.2 超时预算必须先于重试策略&lt;/h3&gt;&#xA;&lt;p&gt;很多团队会先配“重试 3 次”，但没配总预算。结果是每次重试都等满超时，最后一个请求占用十几秒。&lt;/p&gt;&#xA;&lt;p&gt;更稳妥的做法是分两层：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每次尝试超时（per-try timeout）：避免单次卡死。&lt;/li&gt;&#xA;&lt;li&gt;整体调用超时（overall timeout）：限制整次业务调用的总耗时。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在 Resilience Pipeline 里，这两层要通过策略顺序明确表达：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;outer timeout 放在 retry 外层，控制整次调用预算。&lt;/li&gt;&#xA;&lt;li&gt;inner timeout 放在 retry 内层，控制单次 attempt。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;先定预算，再谈重试次数，否则重试是放大器，不是保护器。&lt;/p&gt;&#xA;&lt;h3 id=&#34;23-遵循行业共识重试只能处理瞬时故障不能处理业务冲突&#34;&gt;2.3 遵循行业共识，重试只能处理瞬时故障，不能处理业务冲突&lt;/h3&gt;&#xA;&lt;p&gt;可重试的典型对象是：网络抖动、连接中断、429、部分 5xx。不可重试的是：参数错误、鉴权失败、业务规则冲突。&lt;/p&gt;&#xA;&lt;p&gt;如果把所有非 200 都重试，会把本来可快速失败的请求拖成长尾，最终压垮线程池和连接池。&lt;/p&gt;&#xA;&lt;h3 id=&#34;24-写操作重试前必须定义幂等边界&#34;&gt;2.4 写操作重试前必须定义幂等边界&lt;/h3&gt;&#xA;&lt;p&gt;对 POST/PUT 这类有副作用的请求，重试不是默认安全动作。支付创建、库存扣减、优惠券核销这类写操作，必须先定义幂等键和幂等存储，再启用自动重试。&lt;/p&gt;</description>
    </item>
    <item>
      <title>ASP.NET Core 认证与授权实战：JWT、Policy 与权限边界落地</title>
      <link>https://blog.denglei.me/posts/aspnet-core-jwt-policy-authorization/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/aspnet-core-jwt-policy-authorization/</guid>
      <description>&lt;p&gt;这篇文章不讨论完整身份平台建设，只聚焦 ASP.NET Core 里最常见、也最容易出错的一段：JWT 认证、Policy 授权，以及资源级权限边界该怎么落到代码里。&lt;/p&gt;&#xA;&lt;h2 id=&#34;问题背景&#34;&gt;问题背景&lt;/h2&gt;&#xA;&lt;p&gt;真实现场：一个后台退款接口原本只允许财务角色调用，但线上排查发现，普通运营账号只要拿到有效 token，也能调用成功。&lt;/p&gt;&#xA;&lt;p&gt;根因并不复杂：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;接口加了 &lt;code&gt;[Authorize]&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;系统只校验“是否登录”&lt;/li&gt;&#xA;&lt;li&gt;没有继续校验角色、权限和资源归属&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;结果就是，认证做了，授权却只做了一半。&lt;/p&gt;&#xA;&lt;p&gt;这也是很多系统的共性问题。认证只是在回答“你是谁”，授权回答的是“你能做什么”。如果这两件事没有拆开设计，接口表面安全，实际边界会很模糊。&lt;/p&gt;&#xA;&lt;h2 id=&#34;原理解析&#34;&gt;原理解析&lt;/h2&gt;&#xA;&lt;h3 id=&#34;认证解决身份确认&#34;&gt;认证解决身份确认&lt;/h3&gt;&#xA;&lt;p&gt;认证的目标，是确认当前请求对应的是哪个用户、哪个客户端，常见做法就是校验 JWT 的签名、过期时间、签发方和受众。&lt;/p&gt;&#xA;&lt;p&gt;这一步做完后，系统拿到的是一个 &lt;code&gt;ClaimsPrincipal&lt;/code&gt;。它说明“请求身份可信”，但并不说明这个身份就有所有权限。&lt;/p&gt;&#xA;&lt;h3 id=&#34;授权解决操作范围&#34;&gt;授权解决操作范围&lt;/h3&gt;&#xA;&lt;p&gt;授权是在认证之后，对用户能力做进一步判断。&lt;/p&gt;&#xA;&lt;p&gt;在 ASP.NET Core 里，最常见的落点是 Policy。你可以按角色、权限声明、租户、部门或业务规则定义策略，而不是在控制器里到处手写 if 判断。&lt;/p&gt;&#xA;&lt;h3 id=&#34;角色不等于权限模型&#34;&gt;角色不等于权限模型&lt;/h3&gt;&#xA;&lt;p&gt;很多系统一开始只有 &lt;code&gt;Admin&lt;/code&gt;、&lt;code&gt;Operator&lt;/code&gt;、&lt;code&gt;User&lt;/code&gt; 这几个角色，后来业务一复杂，就会发现角色粒度太粗。&lt;/p&gt;&#xA;&lt;p&gt;更稳妥的方式通常是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;角色用于粗粒度分组&lt;/li&gt;&#xA;&lt;li&gt;权限声明用于精细操作控制&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;例如“财务”和“运营”都属于后台用户，但是否允许退款、导出、调价，应该由权限声明决定，而不是只靠角色名硬编码。&lt;/p&gt;&#xA;&lt;h3 id=&#34;资源级授权才是真正的边界&#34;&gt;资源级授权才是真正的边界&lt;/h3&gt;&#xA;&lt;p&gt;就算用户具备 &lt;code&gt;orders.refund&lt;/code&gt; 权限，也不代表他可以操作所有订单。&lt;/p&gt;&#xA;&lt;p&gt;很多越权问题出在这里：接口只校验了功能权限，没有校验资源归属，比如租户是否匹配、门店是否匹配、是否只能操作自己负责的数据。&lt;/p&gt;&#xA;&lt;p&gt;所以完整授权通常分两层：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;功能级：你有没有这个动作权限&lt;/li&gt;&#xA;&lt;li&gt;资源级：你能不能对这条具体数据执行这个动作&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;示例代码&#34;&gt;示例代码&lt;/h2&gt;&#xA;&lt;p&gt;下面用一个“订单退款接口”来说明一套常见落地方式。&lt;/p&gt;&#xA;&lt;p&gt;先配置 JWT 认证：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;Microsoft.AspNetCore.Authentication.JwtBearer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;Microsoft.IdentityModel.Tokens&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;System.Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;builder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Services&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AddAuthentication&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AddJwtBearer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;options&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TokenValidationParameters&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TokenValidationParameters&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidateIssuer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidateAudience&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidateIssuerSigningKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidateLifetime&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidIssuer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;builder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Jwt:Issuer&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ValidAudience&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;builder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Jwt:Audience&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;IssuerSigningKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Encoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UTF8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;builder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Jwt:SigningKey&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]!)),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ClockSkew&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TimeSpan&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;FromSeconds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;再定义基于权限声明的授权策略：&lt;/p&gt;</description>
    </item>
    <item>
      <title>EF Core 事务实战：SaveChanges、ExecutionStrategy、Outbox 与一致性边界</title>
      <link>https://blog.denglei.me/posts/efcore-transactions-savechanges-executionstrategy-outbox/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/efcore-transactions-savechanges-executionstrategy-outbox/</guid>
      <description>&lt;p&gt;“事务加上了，这可不就问了嘛”。真正到线上一跑，问题就冒出来了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;数据库提交成功了，但消息没发出去&lt;/li&gt;&#xA;&lt;li&gt;外部调用成功了，但本地事务回滚了&lt;/li&gt;&#xA;&lt;li&gt;SQL 重试机制打开后，一段代码被重复执行了两次&lt;/li&gt;&#xA;&lt;li&gt;同一个请求里明明都写在一起，最终状态还是不一致&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这篇文章不讲抽象理论，直接回答一个工程问题：在 EF Core 里，事务边界到底该怎么画，哪些事情能靠数据库事务兜住，哪些事情必须换一种设计。&lt;/p&gt;&#xA;&lt;h2 id=&#34;问题背景为什么写成功了不等于业务真的成功&#34;&gt;问题背景：为什么“写成功了”不等于业务真的成功&lt;/h2&gt;&#xA;&lt;p&gt;先看一个常见场景：下单成功后，需要同时完成三件事。&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;写入订单表&lt;/li&gt;&#xA;&lt;li&gt;扣减库存&lt;/li&gt;&#xA;&lt;li&gt;发出一条订单已创建消息，通知下游做履约&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;很多项目最开始都会写成这样：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tx&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Database&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OrderStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Reserve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;quantity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SaveChangesAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;messageBus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PublishAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OrderCreated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CommitAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码看起来很顺，实际有三个明显风险：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;SaveChangesAsync&lt;/code&gt; 成功后，&lt;code&gt;PublishAsync&lt;/code&gt; 失败，数据库已经提交，但下游不知道这笔订单存在&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;PublishAsync&lt;/code&gt; 成功后，&lt;code&gt;CommitAsync&lt;/code&gt; 失败，下游收到消息，但数据库里没有最终结果&lt;/li&gt;&#xA;&lt;li&gt;如果启用了连接重试，整段逻辑可能被重放，导致消息重复发送&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;问题的根因不是 EF Core 不行，而是数据库事务的能力边界经常被高估了。&lt;/p&gt;&#xA;&lt;p&gt;数据库事务能保证的是“同一个数据库连接里的状态一致”，它保证不了消息队列、HTTP 调用、Redis 写入这些外部副作用跟你一起原子提交。&lt;/p&gt;&#xA;&lt;h2 id=&#34;原理解析ef-core-里的事务到底管到哪一步&#34;&gt;原理解析：EF Core 里的事务到底管到哪一步&lt;/h2&gt;&#xA;&lt;h3 id=&#34;savechanges-默认就带事务但范围很有限&#34;&gt;&lt;code&gt;SaveChanges&lt;/code&gt; 默认就带事务，但范围很有限&lt;/h3&gt;&#xA;&lt;p&gt;对关系型数据库来说，EF Core 在一次 &lt;code&gt;SaveChanges&lt;/code&gt; 中默认会开启事务，确保这一批 insert/update/delete 要么全部成功，要么全部失败。&lt;/p&gt;&#xA;&lt;p&gt;也就是说，下面这种写法本身就已经带事务语义：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OrderStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Reserve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;quantity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SaveChangesAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个默认事务适合的场景很明确：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;同一个 &lt;code&gt;DbContext&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;同一个数据库&lt;/li&gt;&#xA;&lt;li&gt;同一次提交&lt;/li&gt;&#xA;&lt;li&gt;没有外部副作用参与&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;只有当你要把多次 &lt;code&gt;SaveChanges&lt;/code&gt;、多段仓储调用、或跨服务动作绑在一起，就要考虑显式事务。&lt;/p&gt;&#xA;&lt;h3 id=&#34;显式事务解决的是多步数据库写入&#34;&gt;显式事务解决的是“多步数据库写入”&lt;/h3&gt;&#xA;&lt;p&gt;如果业务上需要多次落库，而且这些写入必须成组成功，可以显式开启事务：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tx&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Database&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OrderStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SaveChangesAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;payment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PaymentStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Pending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SaveChangesAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CommitAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这类事务能解决的问题是：多次数据库操作之间不能只成功一半。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
