基于ASP.NET Core MVC与SQL Server构建高可用电商平台全栈实战
简介:在现代Web开发领域,构建一个稳定、可扩展的电商平台是许多开发者和企业面临的核心挑战。其技术原理通常围绕清晰的分层架构展开,通过表现层、业务逻辑层和数据访问层的分离,确保代码的可维护性与团队协作效率。这一架构模式的技术价值在于,它能有效管理复杂的业务逻辑,如订单处理、库存管理和支付集成,同时为性能优化和安全防护奠定基础。在应用场景上,无论是B2C零售、B2B批发还是跨境电商,一个健壮的后台管理系统和流畅的前台购物流程都是不可或缺的。本文将聚焦于使用ASP.NET Core MVC框架与SQL Server数据库这一经典技术组合,深入探讨如何实现包括商品管理、订单系统在内的核心功能模块,分享从数据库设计到高并发处理的实战经验,为搭建企业级电商解决方案提供系统性的指导。
1. 项目概述:从零构建一个现代电商平台
最近几年,无论是创业公司还是传统企业,搭建自己的线上商城几乎成了标配。但一提到“自己开发”,很多人第一反应是头大:技术栈怎么选?数据库怎么设计?支付、订单、库存这些复杂的业务逻辑怎么处理?我过去参与过好几个电商项目,从早期的Web Forms到现在的.NET Core,踩过不少坑,也积累了一些心得。今天,我就以“ASP.NET Core MVC + SQL Server”这套经典且强力的组合为例,跟你聊聊如何系统地搭建一个功能完整、易于维护的商城系统。这不是一个简单的Demo,而是一个考虑了真实业务场景、扩展性和性能的实战方案。无论你是想学习全栈开发,还是为公司启动一个新项目,这篇文章都能给你提供一个清晰的路线图和一堆可以直接“抄作业”的代码思路。
简单来说,我们要构建的系统核心包括:面向用户的前台商品展示、购物车、订单流程;面向管理员的后台商品管理、订单处理、用户管理;以及支撑这一切的数据库、业务逻辑层和安全的支付集成。选择ASP.NET Core MVC是因为它提供了清晰的Model-View-Controller分离模式,对于业务逻辑复杂的电商系统来说,代码结构清晰就是最大的维护性保障。而SQL Server作为老牌关系型数据库,在事务一致性、复杂查询和与.NET生态的集成度上,依然是企业级应用非常可靠的选择。接下来,我会把整个构建过程拆解成几个核心部分,一步步带你深入。
2. 技术栈选型与架构设计思路
为什么是ASP.NET Core MVC + SQL Server?这个选择背后有一系列的权衡。首先,ASP.NET Core是跨平台的,这意味着你的应用可以部署在Linux服务器上,通常能节省不少授权费用。它的性能在众多Web框架中名列前茅,这对于高并发的电商场景(比如秒杀)至关重要。MVC模式强制性地将数据模型、业务逻辑和用户界面分离,这让团队协作变得清晰——前端工程师专注于View,后端工程师处理Model和Controller,数据库专家设计Table,各司其职。
SQL Server的选择,尤其是较新的版本如2019或2022,看中的是其成熟度、强大的T-SQL功能、优秀的执行计划优化器以及完善的备份与高可用方案(如Always On)。对于商城系统,事务(Transaction)是生命线。用户下单扣库存、支付成功更新订单状态,这一系列操作必须在同一个事务里完成,确保数据绝对一致,SQL Server在这方面做得非常扎实。当然,你也可以考虑PostgreSQL或MySQL,但对于已经熟悉.NET生态的团队,SQL Server的集成工具链(如Entity Framework Core提供的第一方支持)和SSMS(SQL Server Management Studio)带来的开发调试便利性,是一个巨大的加分项。
整个系统的架构,我建议采用经典的分层架构,这能有效控制复杂度:
- 表现层(Presentation Layer):即ASP.NET Core MVC项目。Controller处理HTTP请求,调用服务,返回View或API数据。View使用Razor模板引擎,可以方便地混合HTML和C#代码,动态渲染页面。
- 业务逻辑层(Business Logic Layer):这是核心。所有与商城相关的业务规则,如计算优惠券、校验库存、生成订单号、处理支付回调等,都应该封装在这一层的各个Service类中。Controller应该很“薄”,只负责协调和转发,真正的脏活累活都在Service里。
- 数据访问层(Data Access Layer):使用Entity Framework Core作为ORM(对象关系映射器)。它允许你使用C#类(称为Entity)来操作数据库,极大简化了CRUD(增删改查)代码。你可以为每个主要的实体(如Product, Order)定义对应的DbSet和Repository模式,进一步抽象数据访问细节。
- 数据库层(Database Layer):即SQL Server实例。这里存放所有的表、视图、存储过程和索引。
此外,还需要考虑一些横切关注点:
- 身份认证与授权:使用ASP.NET Core Identity。它能快速搭建用户注册、登录、角色管理(如普通用户、管理员)等功能,并集成到MVC的
[Authorize]特性中,轻松控制页面或API的访问权限。 - 缓存:为了缓解数据库压力,提升商品列表、首页等频繁访问页面的加载速度,必须引入缓存。对于分布式部署,Redis是首选;单机部署可以用IMemoryCache。
- 日志与监控:使用ILogger接口配合Serilog等库,将系统运行日志、错误信息记录到文件或数据库,便于问题排查。
注意:在架构初期,切忌过度设计。很多团队一开始就想着要支持微服务、事件总线,结果把简单问题复杂化。对于大多数中小型商城,一个结构清晰、模块化的单体应用(Monolithic)配合良好的分层,完全能够支撑初期的业务发展,并且开发和部署成本低得多。等业务量真的上来了,再对有瓶颈的模块进行拆分也不迟。
3. 数据库设计与核心表结构解析
数据库设计是商城系统的基石,设计得好,后期开发顺风顺水;设计得差,改表如改命。我们的核心实体包括:用户、商品、商品分类、购物车、订单、订单明细、收货地址、支付记录等。下面我详细拆解几个最关键的表的设计思路和注意事项。
3.1 商品与分类体系设计
商品表(Products)是业务的中心。除了Id、Name、Description、Price这些基础字段,有几个点需要特别注意:
- SKU管理:
SKU(Stock Keeping Unit)字段是商品的唯一库存标识。同一款衣服的不同颜色、尺码,应该是不同的SKU,对应不同的库存。StockQuantity(库存数量)字段与SKU绑定。 - 价格字段:至少需要
Price(原价)和SalePrice(售价)。促销时,展示SalePrice。可以考虑增加CostPrice(成本价)用于计算毛利。 - 图片存储:不建议在数据库中直接存图片的二进制数据(BLOB),这会让数据库体积暴涨,影响备份和查询性能。最佳实践是在
Product表中存图片的URL路径(如string MainImageUrl),将图片文件实际存储在对象存储服务(如阿里云OSS、腾讯云COS)或服务器的特定目录下。 - 软删除:增加一个
IsDeleted的bit类型字段,默认为0。删除商品时,只是将此字段标记为1,而不是物理删除记录。这可以避免误删,也便于数据审计和恢复。
分类表(Categories)通常设计为支持无限级树形结构。一个简单实用的设计是使用“父Id”方式:
CREATE TABLE Categories ( Id INT PRIMARY KEY IDENTITY(1,1), Name NVARCHAR(100) NOT NULL, ParentId INT NULL, -- 指向父分类的Id,顶级分类的ParentId为NULL DisplayOrder INT NOT NULL DEFAULT 0, -- 用于同级分类的排序 IsActive BIT NOT NULL DEFAULT 1, FOREIGN KEY (ParentId) REFERENCES Categories(Id) );商品与分类是多对多关系,因为一个商品可以属于多个分类(例如,一款手机既属于“电子产品”,又属于“数码配件”)。这就需要一张关联表ProductCategories,包含ProductId和CategoryId两个外键。
3.2 订单系统的核心与难点
订单系统是电商最复杂的一环,核心是Orders和OrderItems两张表。
Orders表记录订单概要:OrderNumber(唯一订单号,通常由日期+随机数生成)、UserId、TotalAmount(订单总金额)、Status(订单状态,如:待支付、已支付、已发货、已完成、已取消)、ShippingAddress(收货地址快照,因为用户可能修改地址,这里必须存下单时的副本)、PaymentMethod、CreatedTime等。OrderItems表记录订单中的每一项商品:OrderId、ProductId、ProductName(商品名称快照)、UnitPrice(下单时单价快照)、Quantity。
这里最大的“坑”在于数据快照。为什么OrderItems里要存ProductName和UnitPrice,而不是直接去Products表里关联查询?因为商品信息(名称、价格)可能会变。如果用户下单后,管理员修改了商品名称或价格,用户查历史订单时,看到的必须是下单那一刻的信息,否则会产生纠纷。所以,在生成订单项时,必须把当时的关键信息“冻结”下来。
订单状态流转是另一个关键点。状态必须单向、明确地流转。例如,“已发货”的订单不能直接变回“待支付”。在代码中,通常用一个枚举(Enum)来定义所有状态,并在改变状态的业务逻辑里进行严格校验。
3.3 购物车与用户数据的关联
购物车(ShoppingCartItems)设计相对简单,主要字段有:Id、UserId(或CartId,未登录用户可用临时ID)、ProductId、Quantity、AddedTime。它本质上是一个临时性的数据,当用户下单后,对应的购物车项就应该被清除或转移到订单中。
用户表除了集成ASP.NET Core Identity提供的标准字段(Id,UserName,Email,PasswordHash等),我们通常还需要扩展,比如添加PhoneNumber、AvatarUrl(头像)、NickName等。收货地址可以单独设计一张UserAddresses表,与用户关联。
实操心得:关于索引,一定要在
WHERE、JOIN、ORDER BY子句中频繁出现的字段上建立索引。例如,Products表的CategoryId、IsDeleted;Orders表的UserId、Status、CreatedTime。但索引不是越多越好,每个索引都会增加写操作的开销。可以使用SQL Server Management Studio (SSMS) 的“执行计划”功能来分析查询效率,针对性优化。
4. 后台管理功能的实现要点
后台管理是商城的“大脑”,需要一个清晰、高效的操作界面。我们可以直接在同一个ASP.NET Core MVC项目中,通过区域(Area)功能来划分前台和后台。创建一个名为Admin的Area,所有后台的Controller、View都放在这个区域下。
4.1 商品管理的增删改查(CRUD)
这是后台最基本也是最频繁的操作。使用Entity Framework Core,实现起来非常模式化。
- 列表页:在
Admin/ProductController的Index动作中,查询Products表,通常需要支持分页、按名称搜索、按分类筛选。这里强烈建议使用服务器端分页,即每次只从数据库查询一页的数据,而不是把所有数据都拉到内存再分页。可以使用Skip()和Take()方法,或者一些现成的分页库(如X.PagedList)。 - 创建与编辑页:共用一个视图(Create/Edit)。表单中需要处理分类的多选(通常用
<select multiple>标签)、图片上传等。图片上传的典型流程是:前端通过<input type="file">选择文件,通过FormData提交到后端一个专门的API接口;后端接口将文件保存到磁盘或云存储,生成一个访问URL,然后将这个URL返回给前端;前端再将这个URL作为表单的一个隐藏字段值,随其他商品信息一起提交到保存商品的Action。 - 删除操作:如前所述,实现软删除。在Service层,将对应商品的
IsDeleted标记为true,而不是调用DbContext.Remove()。
一个常见的痛点是大批量商品上架或修改。这时可以考虑实现Excel导入导出功能。使用EPPlus或NPOI库,可以方便地读取Excel文件数据并批量插入数据库,或者将查询结果导出为Excel供运营人员下载。
4.2 订单处理与状态管理流
后台订单列表需要提供强大的筛选功能:按订单号、按用户、按时间范围、按状态等。订单详情页需要展示订单的所有信息,包括订单概要和所有订单项,以及最重要的——状态操作按钮。
状态操作是后台订单管理的核心交互。例如,客服点击“发货”按钮,需要弹出一个模态框(Modal)让客服填写物流公司和运单号。提交后,后端逻辑是:
- 验证订单当前状态是否为“已支付”。
- 更新
Orders表的Status为“已发货”。 - 在
OrderShipments表(如果有)中插入一条发货记录,包含物流信息。 - 可选但强烈推荐:向用户发送通知,如短信、邮件或App推送,告知订单已发货。
所有状态变更操作,都必须记录操作日志(OrderLogs表),记录操作人、操作时间、从什么状态变为什么状态、备注信息。这在出现纠纷时是至关重要的审计依据。
4.3 用户、权限与数据统计
用户管理可以直接利用ASP.NET Core Identity提供的UI和API进行扩展,管理用户列表、锁定账户、重置密码等。
权限控制使用基于角色的授权(Role-Based Authorization)。在Startup.cs或Program.cs中配置好角色(如“Admin”, “ContentManager”, “OrderManager”),然后在后台Controller或Action上使用[Authorize(Roles = “Admin”)]特性。更细粒度的权限可以使用策略(Policy)来实现。
数据统计仪表盘是提升运营效率的关键。后台首页应该展示一些核心KPI图表,例如:
- 今日/本月订单数、销售额
- 热销商品排行榜
- 用户注册趋势图
- 订单状态分布饼图
这些数据可以通过编写复杂的SQL查询,或者使用Entity Framework Core的GroupBy、Sum等LINQ操作来获取。对于实时性要求不高但计算复杂的统计,可以考虑定期(如每天凌晨)通过后台任务(如Hangfire、Quartz.NET)计算好,存入Statistics汇总表,前端直接读取,以提升页面加载速度。
5. 前台用户购物流程的关键实现
前台是用户直接接触的部分,体验必须流畅。核心流程包括:浏览商品、加入购物车、结算下单、支付。
5.1 商品列表、搜索与详情页优化
商品列表页的查询性能是首要优化点。除了数据库索引,EF Core查询时要注意:
- 使用
AsNoTracking():如果只是展示数据,不需要更新,那么查询时加上.AsNoTracking(),EF Core不会为实体创建状态跟踪,能提升查询速度并降低内存消耗。 - 警惕N+1查询问题:例如,列表要显示商品分类名。如果先查询商品列表,再循环中查询每个商品的分类,就会产生N+1次数据库查询。正确做法是使用
.Include(p => p.Category)在第一次查询时就通过Join一次性加载关联数据。 - 分页必须做:无论有多少商品,列表必须分页。使用
Skip((pageIndex-1)*pageSize).Take(pageSize)。
搜索功能通常基于商品名称、简介、SKU等字段。简单的使用WHERE Name.Contains(keyword),但对于中文分词和复杂搜索,集成Elasticsearch或SQL Server的全文检索功能是更专业的方案。
商品详情页要特别注意库存显示。库存数量需要实时从数据库查询,但在高并发下,频繁查询StockQuantity可能成为瓶颈。一个优化方案是:在详情页展示一个缓存的值(如Redis中),这个缓存可以有一定延迟(比如5秒更新一次),而在用户真正加入购物车或下单时,再实时校验并锁定数据库中的真实库存。
5.2 购物车状态管理与订单提交
购物车需要区分用户是否登录。
- 未登录状态:可以使用浏览器的
localStorage或sessionStorage来存储购物车项,或者在后端给用户分配一个临时的Guid作为CartId,存入Cookie,并在数据库中用这个CartId来关联购物车项。 - 用户登录后:需要将临时购物车(
localStorage或临时CartId下的商品)合并到该用户的数据库购物车记录中。这个合并逻辑通常在登录成功的回调里执行。
提交订单(Checkout)是整个流程中最复杂的一步,涉及事务和并发控制。
- 验证:验证收货地址、支付方式是否有效。
- 库存预检查:遍历购物车中每一项,检查
Products表中对应SKU的StockQuantity是否大于等于购买数量。如果任何一项库存不足,立即返回错误。 - 创建订单(核心事务):这里必须将“扣减库存”和“创建订单”放在同一个数据库事务中,确保原子性。
using var transaction = await _context.Database.BeginTransactionAsync(); try { // 1. 创建订单主记录 var order = new Order { ... }; await _context.Orders.AddAsync(order); await _context.SaveChangesAsync(); // 获取Order.Id // 2. 创建订单项,并扣减库存 foreach (var cartItem in cartItems) { var product = await _context.Products.FindAsync(cartItem.ProductId); // 再次检查库存(防止在第一步检查后、此时之前被其他请求修改) if (product.StockQuantity < cartItem.Quantity) { throw new Exception($"商品 {product.Name} 库存不足"); } product.StockQuantity -= cartItem.Quantity; // 扣减库存 var orderItem = new OrderItem { OrderId = order.Id, ProductId = product.Id, ProductName = product.Name, // 快照! UnitPrice = product.SalePrice, // 快照! Quantity = cartItem.Quantity }; await _context.OrderItems.AddAsync(orderItem); } // 3. 清空该用户的购物车 _context.ShoppingCartItems.RemoveRange(cartItems); await _context.SaveChangesAsync(); // 保存所有更改(订单项、库存更新、购物车清除) await transaction.CommitAsync(); // 提交事务 // 4. 跳转到支付页面,传入订单号 return RedirectToAction("Payment", new { orderId = order.Id }); } catch (Exception ex) { await transaction.RollbackAsync(); // 记录日志,返回错误信息给用户 return View("CheckoutError”, model: ex.Message); }重要提示:在高并发场景下,即使使用事务,上述代码的“查询-判断-更新”库存模式仍可能引发超卖。更严谨的做法是使用数据库的悲观锁(如
SELECT ... WITH (UPDLOCK))或乐观并发控制(使用RowVersion字段)。对于秒杀场景,则需要更复杂的方案,如将库存提前扣减到Redis中,再用异步任务同步回数据库。
5.3 支付接口集成与回调处理
集成支付(如支付宝、微信支付)是标准流程。以支付宝网页支付为例:
- 下单:用户点击支付,你的服务器端向支付宝接口发起请求,生成一个支付订单,并获取返回的支付页面链接(
form表单或URL)。 - 跳转:将用户浏览器重定向到这个支付宝支付页面。
- 异步回调(最关键):用户支付成功后,支付宝服务器会主动向你预先设置好的一个后台通知地址(Notify URL)发送POST请求,告诉你支付结果。你必须在这个回调接口里处理业务逻辑。
- 同步跳转:支付完成后,支付宝会将用户浏览器重定向回你设置的一个返回页面(Return URL),这个页面通常只是展示支付成功/失败的结果,不应在此处处理核心业务逻辑,因为用户可能不点击返回,或者网络问题导致跳转失败。
回调接口的处理逻辑必须是幂等的(即同一笔支付通知多次调用,结果一致):
[HttpPost(“/notify/alipay”)] public async Task<IActionResult> AlipayNotify() { // 1. 验证签名,确保请求确实来自支付宝(非常重要,防止伪造请求!) bool signVerified = VerifySignature(Request.Form); if (!signVerified) return BadRequest(“签名验证失败”); // 2. 解析回调参数,获取商户订单号(out_trade_no)和支付宝交易号(trade_no) string outTradeNo = Request.Form[“out_trade_no”]; string tradeStatus = Request.Form[“trade_status”]; // 3. 根据outTradeNo查询本地订单 var order = await _orderService.GetOrderByNumberAsync(outTradeNo); // 4. 判断订单状态,避免重复处理 if (order != null && order.Status == OrderStatus.PendingPayment) { if (tradeStatus == “TRADE_SUCCESS” || tradeStatus == “TRADE_FINISHED”) { // 5. 在数据库事务中,更新订单状态为“已支付”,并记录支付流水号等 await _orderService.ProcessPaidOrderAsync(order.Id, tradeNo); // 6. 触发后续动作:更新库存(如果下单时未扣)、发送邮件/短信通知等 await _inventoryService.UpdateStockFromOrderAsync(order.Id); await _notificationService.SendPaymentSuccessEmailAsync(order.UserEmail, order.OrderNumber); } } // 7. 无论处理成功与否,都必须返回“success”给支付宝,否则支付宝会认为通知失败,反复调用 return Content(“success”, “text/plain”); }6. 部署、性能优化与安全考量
项目开发完,部署上线才是真正的开始。对于ASP.NET Core应用,可以发布为自包含(Self-Contained)或框架依赖(Framework-Dependent)的部署包,部署到IIS、Nginx反向代理后,或者直接使用Kestrel运行。
6.1 SQL Server连接与配置管理
连接字符串不要硬编码在appsettings.json里,尤其是生产环境。应该使用环境变量(如ASPNETCORE_ENVIRONMENT=Production)来区分不同环境的配置,并将生产环境的连接字符串、API密钥等敏感信息存储在Azure Key Vault、环境变量或安全的配置中心。
在Program.cs中配置DbContext时,建议启用连接池和更精细的重试策略,以应对网络波动:
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString(“DefaultConnection”), sqlOptions => { sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); } ));6.2 缓存策略与性能提升实战
缓存是提升性能的利器,要用在刀刃上。
- 场景一:商品分类菜单:几乎全站每个页面都要显示,变化不频繁。适合用
IMemoryCache或IDistributedCache(如Redis)缓存,设置一个较长的过期时间(如30分钟),并在后台管理分类时主动清除缓存。 - 场景二:首页热销商品:可以缓存整个渲染好的HTML片段(Output Caching),或者缓存查询结果。
- 场景三:用户会话:将用户的购物车信息、登录状态等存入分布式缓存,可以实现多台Web服务器间的会话共享。
使用缓存时,必须考虑缓存穿透(查询一个不存在的数据,导致每次请求都打到数据库)和缓存雪崩(大量缓存同时失效,请求全部涌向数据库)。对于穿透,可以将空结果也缓存一小段时间;对于雪崩,可以为缓存过期时间加上一个随机值。
6.3 必须关注的安全防护点
电商系统涉及金钱和用户隐私,安全是重中之重。
- SQL注入:使用EF Core这样的ORM,本身已经通过参数化查询很大程度上避免了SQL注入。绝对不要用字符串拼接的方式构造SQL。
- XSS跨站脚本攻击:Razor视图默认会对输出进行HTML编码,这很好。但对于用户提交的、后来又展示给其他用户的内容(如商品评论),要格外小心,可以使用白名单过滤HTML标签,或者使用专门的防XSS库。
- CSRF跨站请求伪造:ASP.NET Core MVC默认内置了防伪令牌(Anti-Forgery Token)验证。确保在表单中使用
@Html.AntiForgeryToken(),并在对应的[HttpPost]Action上使用[ValidateAntiForgeryToken]特性。 - 敏感数据保护:用户的密码必须加盐哈希存储(ASP.NET Core Identity已默认处理)。不要在日志、URL或响应中泄露用户ID、订单号等敏感信息。支付相关的通信必须使用HTTPS。
- 上传文件安全:对用户上传的图片,要进行文件类型检查(检查MIME类型或文件头,而非仅扩展名),并重命名存储,防止恶意脚本上传和执行。
7. 开发与运维中的常见问题排查
即使设计得再完善,实际开发和上线后总会遇到各种问题。这里记录几个我踩过的典型“坑”和解决方法。
7.1 数据库连接与性能问题
问题:应用运行一段时间后,出现“连接池耗尽”或查询超时的错误。排查:
- 检查代码中是否每个数据库操作都正确使用了
using语句或依赖注入来确保DbContext被及时释放。未释放的DbContext会一直占用连接。 - 使用SSMS的活动监视器或扩展事件,查看当前有哪些耗时长的查询。优化这些查询,比如添加缺失的索引、重写复杂的子查询为JOIN。
- 检查是否在循环中进行了大量的单条查询(N+1问题),应改为批量查询或使用
.Include()。 - 适当增加连接池大小(在连接字符串中设置
Max Pool Size),但这只是缓解,根本还是要解决连接泄露或慢查询。
问题:实体跟踪(Tracking)导致的内存和性能问题。解决:对于只读查询,务必使用.AsNoTracking()。例如:_context.Products.Where(p => p.IsActive).AsNoTracking().ToListAsync()。
7.2 支付回调与订单状态同步
问题:用户付了款,但订单状态还是“待支付”。排查:
- 检查回调日志:你的支付回调接口必须有详细的日志记录,记录接收到的所有参数、验证结果、处理过程。这是排查问题的第一手资料。
- 验证签名:99%的回调问题源于签名验证失败。确保你从支付宝/微信支付后台下载的是正确的公钥,并且验签算法与支付平台要求的一致。
- 网络与防火墙:确保你的回调接口地址(Notify URL)能从公网访问,并且服务器的防火墙没有屏蔽支付平台IP发来的请求。
- 幂等性检查:检查回调逻辑是否因为网络重试导致了重复更新。确保你的处理逻辑是幂等的,即根据支付宝交易号或商户订单号,先判断订单是否已处理过。
- 手动补单:后台需要提供一个功能,允许运营人员输入支付宝交易号,手动查询支付状态并更新本地订单。这是线上应急的必备工具。
7.3 库存超卖与并发控制
问题:在促销时,商品库存被扣成了负数。解决:
- 悲观锁:在扣减库存的查询中使用
WITH (UPDLOCK, ROWLOCK)提示,锁定该行数据,直到事务结束。这能保证绝对安全,但并发度高时可能造成大量阻塞。var product = await _context.Products .FromSqlInterpolated($”SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) WHERE Id = {productId}”) .FirstOrDefaultAsync(); - 乐观并发:在
Product实体上增加一个RowVersion(时间戳)字段。更新时,EF Core会在WHERE子句中包含这个版本号,如果更新时发现版本号与读取时不一致(说明数据已被其他事务修改),则会抛出DbUpdateConcurrencyException,你可以在捕获异常后重试或提示用户。 - 应用层队列:将下单请求放入消息队列(如RabbitMQ、Azure Service Bus),由单个或多个消费者顺序处理,从源头控制并发。这对架构改动较大,适用于秒杀等极端场景。
- 预扣库存:在用户提交订单但未支付时,先锁定库存(设置一个
LockedStock字段),支付成功后再扣减真实库存,支付超时(如15分钟)则释放锁定的库存。这能更好地反映真实可售库存。
7.4 日志记录与错误追踪
没有完善的日志,线上问题就是盲人摸象。除了使用ILogger,建议集成像Serilog这样的强大日志库,它可以轻松配置将日志同时输出到控制台、文件和像Seq、ELK这样的日志聚合系统。记录日志时要注意分级:
Information:记录正常的业务流水,如“用户{UserId}创建了订单{OrderNumber}”。Warning:记录异常但可处理的情况,如“库存不足,商品{ProductId}”。Error和Critical:记录系统错误和未处理的异常,务必包含完整的异常堆栈和上下文信息。
对于分布式环境,一个请求可能经过多个服务,使用像OpenTelemetry这样的技术来生成和传递唯一的TraceId,可以将分散的日志串联起来,完整还原一次请求的调用链,这对排查复杂问题至关重要。
本文还有配套的精品资源,点击获取
