2024年7月

1 互联网架构越来越复杂?

为啥感觉互联网架构越来越复杂了,早期我们的系统,可能也就那么少部分人使用,大都是一些后台管理系统。
所以不用考虑很多东西,比如:

  • 流量少,无需考虑并发问题
  • 数据少,不用考虑什么索引优化、分库分表
  • 访问不集中,不用考虑缓存、过载保护
  • 如果数据不重要,不用考虑安全策略,甚至不用考虑容灾备份
  • 可重复提交,所以不用关系幂等性
  • 允许短暂宕机和定期关停维护,所以不用考虑多活架构

但是随着互联网的普及和用户的激增,为了应对流量增量带来的各种问题,我们的架构体系衍生出很多强大的技术方案。

2 什么是秒杀/竞拍业务

秒杀业务也是随着互联网电商的发展而不断普及的,我们来看看普通业务和秒杀业务的区别

2.1 普通的业务

  1. 微信的个人信息:个人的注册信息,公众号、视频号的基础信息,微信好友列表,微信群列表。
    这种是 1:1 的,一般也不会被别人看到。
  2. 微信朋友圈:你盆友圈公开的内容是可以被多个好友看到的,你也可以对应看到你多个好友的盆友圈。
    这种是 1:n 的,多读的一种场景。

2.2 秒杀/竞拍业务

只有少量的数据,却会在集中的时间段被一批人看到和抢购,集中式的高频读写。
业内也称为
群蜂请求
,你可以想象下你捅了马蜂窝的场景。哈哈哈

典型秒杀/竞拍业务案例:

  1. 春运前的火车票开售那一刻,可能瞬间有千万级请求涌入
  2. 将来某个遥遥领先开售,可能是一秒售罄

这些业务场景有如下技术难点:

  1. 瞬时流量特别大
    ,你的接入层、应用层、数据层等能否扛得住
  2. 大量流量涌入
    对一个数据进行操作,怎么保证数据原子增减、顺序公平性
    ,怎么保证数据不超卖
  3. 如何
    保证数据安全
    ,如防攻击、防刷数、保持幂等
  4. 如果使用
    并发控制,如何保证不产生死锁

所以,一个优秀的秒杀业务架构,在现在的互联网业务中,是一个永不过时的话题

3 如何优化

这边只针对几个对秒杀业务有效改进的点做展开,什么集群动态扩容、流量控制、弹性伸缩、智能限流啊,可以参考我的这篇文章《
千万级流量冲击下,如何保证极致性能
》。

3.1 清除无效请求

尽量在前面就把一些无效请求给清理掉,所以这些操作Web前端 或者 App Client端做就行了,越前端越好,尽量不要伤害到服务端,比如:

  • 未登录拦截
  • 重复提交拦截
    (未响应则按钮置灰,直至响应或者5S超时才恢复,幂等保证)
  • 频繁提交拦截
    (单用户一分钟不超过100次,避免AI刷机)
  • 验证码拦截
    (避免AI刷数据、黑客攻击等)
  • 参与条件拦截(可提前加载名单)
    :如用户等级不够、注册未满3个月、用户进入黑名单等

image

3.2 服务端+缓存层做高效原子操作

公共数据做缓存
缓存是提升系统性能的重要手段。通过缓存热点数据,缓存还可以提高数据的访问速度,见很少对数据库的访问速度,提升用户体验。Redis单机每秒10w没什么问题,再加上多集群多副本模式。

原子操作保证秒杀的计数
在Redis中,高效地进行原子计数通常使用
INCR

INCRBY

DECR

DECRBY
等命令。这些命令都是原子操作,意味着在执行时不会被其他Redis命令打断,从而保证了计数的准确性和一致性。

# 计算已售卖1000台库里南
> INCRBY cullinan_counter 1000

# 获取当前售卖数量
> GET cullinan_counter
> 1000

# 超过1000,返回秒杀失败

队列保证请求有序进入
使用Redis的 Stream 队列功能。Stream 实际上是一个 key,你可以使用 XADD 命令向其中添加消息。

XADD mystream * field1 value1 field2 value2

这里 mystream 是 Stream 的名称,* 表示让 Redis 自动生成一个唯一的消息 ID。field1 value1 和 field2 value2 是消息的内容,你可以根据需要添加任意数量的字段。
如果你只有1000台库里南供抢购,那么第1001就不要进入队列了。

扩展阅读
缓存可以扩展阅读作者的这个系列的文章:
★ Redis24篇集合

image

3.3 数据层做终兜底

经过上面的保证之后,到数据层的量就很少了,大概率就是你定额的商品数量同等的数量。
比如1000,数据库绝对的扛得住的。
唯一可以做的就是检查数量是否符合预期,这个可以创建约束或者触发器来实现。

image

3.4 全球式业务,单元化处理

有些人可能会说,我的商品全球售卖,那我的缓存中心、数据中心放哪里,如果放中国,那跨地域跨机房访问,在0.1微妙都能决定我是不是买得到,欧洲的客户铁定抢不到库里南了。
现在的做法一般是单元化隔离,比如:

image

A/B中心都有这样的缓存或者数据结构,配置中心统一下发配置。然后在各自的单元里面玩耍,互不干预。
秒杀业务千万不要想着跨地域+跨机房,用户存在不公平性。

4 写在最后

  1. 无效请求拦截,尽量在前端完成,避免走入后端,造成服务端压力
  2. 缓存支持高性能检索、原子计算和有序队列
  3. 数据层做存储兜底
  4. 分治原理:单元化隔离,避免集中处理

本文分享自华为云社区
《GaussDB(for MySQL)创新特性:灵活多维的二级分区表策略》
,作者:GaussDB 数据库。

背景介绍

分区表及二级分区表的功能,可以让数据库更加有效地管理和查询大规模数据,传统商业数据库具备该能力。MySQL支持分区表,与传统商业数据库相比,MySQL对二级分区表功能的支持尚显不足,存在一定的功能差距。

为了弥补这一差距,GaussDB(for MySQL)发布了对二级分区表功能的支持。这一举措旨在让MySQL用户能够享受到与传统商业数据库类似的分区管理和查询优势,提高数据库的灵活性和性能。

GaussDB(for MySQL)致力于为用户提供更全面、更强大的数据库解决方案,满足其日益增长的数据管理需求。

分区表

众所周知,分区表就是从逻辑上对一个表划分成多个分区,实现物理上的隔离或性能上的优化。GaussDB(for MySQL)继承了开源社区的分区表功能,能够为各个分区定义不同的引擎以及表空间等属性,方便用户管理。

随着表数据的膨胀,单纯的一个表很容易出现性能问题。如随着数据量的增加,查询数据量可能会随之变大,进而导致同一条查询语句性能也会随之下降。而分区表提供了解决大表问题的一个手段,将一张大表分成不同的分区,按照分区定义,合理的书写查询语句,可以使得数据量变化的情况下,查询性能的稳定。

另外一种情况就是,随着时间的推移,表数据中会出现冷数据,如何管理冷数据以及控制冷数据对查询性能的影响,分区表也是一个不错的手段。将冷数据归档到某个或者某几个分区,查询的时候只需查询热分区内的数据,可以避免对冷数据的影响。

一个分区表结构图如下所示:

1.png

二级分区表

二级分区,又称为子分区,是在一级分区的基础上进一步对数据进行细分的技术。在数据库表中,可以根据一个或多个字段的值将数据划分为不同的分区,这就是一级分区。而二级分区则是在一级分区的基础上,再根据其他字段的值对每个一级分区内的数据进行细分。

通过二级分区,可以将数据表划分为更小、更易于管理的片段,从而提高数据的存储效率和查询性能。具体来说,二级分区有以下优势:

  1. 灵活的数据管理:通过二级分区,可以根据业务需求和数据特点灵活地定义分区策略,实现数据的按需存储和查询。
  2. 提高查询效率:由于二级分区将数据划分为更小的片段,因此,在查询时可以只扫描相关的分区,减少不必要的数据扫描,从而提高查询速度。
  3. 便捷的数据备份和恢复:通过二级分区,只需备份或恢复特定的分区,而不是整个数据表,节省备份和恢复所需的时间和空间。

二级分区表更多的是从维度来考虑。当一级分区的数据量开始增加时,可以从另一个维度对其进行管理。例如,在一个销售情况表中,一级分区可以按地区进行划分,而二级分区可以按年份进行进一步划分。这样,当需要查询某地某年的情况时,只需访问二级分区内的数据,就可以获得很好查询的性能。

二级分区结构图如下:

22.png

分区表的优点

  1. 支持在分区级别而不是在整个表上进行数据加载、索引、创建和重建,以及备份和恢复等数据管理操作,可以大幅减少操作时间。
  2. 提高查询性能。通常可以通过访问分区的子集而不是整个表来获得查询的结果。对于某些查询,分区修剪技术可以提供数量级的性能提升,减少无效IO访问。
  3. 分区维护操作的分区独立性,允许用户对同一表或索引的一些分区执行维护操作,而同时保证无运维操作的分区运行并发和DML操作不受影响。查询以及DML和DDL支持并行执行。
  4. 如果将关键表和索引划分为分区以减少维护窗口,则可以提高关键应用的数据库可用性。
  5. 无需重写应用就可以利用分区能力。
  6. 更容易的数据生命周期管理能力。

特性介绍

1. 增强MySQL二级分区类型

GaussDB(for MySQL)对分区表类型进行了增强,组合分区中的二级分区支持更多类型(Range/List/Hash),满足客户不同场景、不同数据类型(如时间)。

11.PNG

2.
支持List Default [Hash]

List Default [Hash] 分区是一种高级分区策略,用于优化数据管理和查询性能,特别是在处理长尾数据和多样化类别数据时。以下是对List Default [Hash] 分区的作用和应用场景的详细描述:

1
)数据隔离

  • 主要数据类别独立:通过列表分区(List Partitioning),可以将主要的数据类别(如特定类型的日志、订单状态、设备类型等)分离到独立的分区中。这样可以确保主要类别的数据在查询时不受其他类别数据的干扰,提高查询效率。
  • 长尾数据管理:未明确分类的数据(通常是长尾数据)被放入默认分区(Default Partition),使得长尾数据与主要类别数据分离管理,减少了对主要数据类别的影响。

2
)数据均匀分布

  • 哈希分区(Hash Partitioning):将默认分区中的数据按哈希算法均匀分布到多个子分区中。这种方式可以避免数据在单一分区中过于集中,防止单一分区成为性能瓶颈,提升查询和管理效率。

3
)查询优化

  • 减少扫描范围:在查询时,只需扫描相关的分区,而不需要扫描整个表的数据,从而显著减少I/O操作,提高查询性能。
  • 平衡负载:通过哈希分区将长尾数据均匀分布在多个分区中,避免某个分区的数据量过大,提升数据库的并发处理能力和响应速度。

4
)灵活数据管理

  • 独立维护:不同分区的数据可以独立进行维护、备份和归档,提升数据管理的灵活性和效率。
  • 动态扩展:当数据量增加时,可以动态增加分区,通过调整哈希分区的数量来平衡负载和优化性能。

3. 支持RANGE INTERVAL 分区

RANGE INTERVAL分区是一种基于范围的分区策略,其中数据根据指定的范围进行分区,并且可以自动创建新的分区以处理未来的数据。这种方法特别适用于时间序列数据等连续增长的数据集。下面是RANGE INTERVAL 分区的优势:

1)自动分区:不需要手动定义每个分区,当新数据超出现有分区范围时,数据库会自动创建新的分区。

2)减少DBA工作量:DBA 不再需要频繁地监控数据增长并手动创建分区,系统会根据数据量动态调整分区。

3)查询优化:查询时只需扫描相关的分区,而不是整个表,从而提高查询效率。

4
)易于管理:简化了数据管理,尤其是对于不断增长的数据集。

分区表的应用场景

分区表可以更好的应用到哪些用户场景?

日志数据分析

应用场景:

  • 服务器日志管理:在大型网站或应用中,服务器每天会生成大量的访问日志、错误日志等,使用分区表可以按天、按类型(访问日志、错误日志)进行分区,也可以先按日期进行一级分区,然后按日志类型(如访问日志和错误日志)进行二级分区。
  • 安全日志分析:在安全系统中,需要对大量的安全事件日志进行分析,通过分区表按事件时间、事件类型(入侵检测、登录失败等)进行分区,可以快速定位并分析特定时间段或类型的安全事件,也可以先按事件时间进行一级分区,然后按事件类型(如入侵检测、登录失败)进行二级分区。
  • 应用性能监控:在应用性能监控系统中,每天会产生大量的性能数据日志,通过分区表按应用模块、时间进行分区,可以快速分析某一模块在特定时间段的性能表现。也可以按照应用模块进行一级分区,然后按照时间段进行二级分区。

优点:

  • 快速查询:针对特定时间段或类型的日志进行查询时,只需扫描相关分区,避免全表扫描,极大提高查询速度。
  • 存储管理:不同时间段的日志数据可以按需保留或删除,便于数据归档和管理。

电商交易数据分析

应用场景:

  • 订单管理:电商平台每天会产生大量的订单数据,通过分区表按订单号、交易时间、用户ID等进行分区,可以快速查询某个用户的所有订单或特定时间段的订单。也可以先按交易日期进行一级分区,再按用户ID或订单状态(如待支付、已支付、已发货)进行二级分区。
  • 用户行为分析:分析用户的购物行为,例如用户在某一时间段内的购买频次、偏好等,通过分区表按用户ID、行为时间进行分区,有助于精准营销。也可以先按行为时间进行一级分区,然后按用户ID或行为类型(如浏览、下单、支付)进行二级分区。
  • 库存管理:电商平台需要实时监控库存,通过分区表按商品ID、时间进行分区,可以快速查询某个商品在不同时期的库存变动情况。也可以先按行为时间进行一级分区,然后按商品ID进行二级分区。

优点:

  • 高效查询:针对某用户或某时间段的交易数据查询时,减少不必要的数据扫描,提高查询效率。
  • 数据管理:按时间分区可以方便地进行历史数据归档和删除,优化存储空间。

物联网数据分析

应用场景:

  • 设备状态监控:物联网系统中需要实时监控设备状态,通过分区表按设备ID、时间戳进行分区,可以快速获取设备的实时数据或历史数据。也可以先按设备ID进行一级分区,再按数据时间戳进行二级分区。
  • 环境数据监测:在环境监测系统中,需要对不同地点的环境数据进行监测,通过分区表按地点ID、时间戳进行分区,可以快速查询某一地点的环境变化情况。也可以先按监测地点进行一级分区,然后按数据时间戳进行二级分区。
  • 智能家居:智能家居设备生成大量的数据,如温度、湿度、用电量等,通过分区表按设备ID、时间进行分区,可以有效管理和分析家庭设备的数据。也可以先按行为时间进行一级分区,然后按设备ID进行二级分区。

优点:

  • 实时性:可以快速查询某个设备的实时数据,满足实时监控需求。
  • 历史分析:方便查询和分析设备的历史数据,识别长期趋势和异常情况。

金融行业数据分析

应用场景:

  • 账户管理:金融机构需要管理大量的账户信息和交易记录,通过分区表按账户类型(个人账户、企业账户)、交易时间进行分区,可以快速查询某个账户的交易记录。也可以先按账户类型(如个人账户、企业账户)进行一级分区,再按交易时间进行二级分区。
  • 风险控制:在风险控制中,需要分析大量的交易数据,识别异常交易,通过分区表按交易时间、交易类型进行分区,可以高效定位并分析可疑交易。也可以先按交易时间进行一级分区,然后按交易类型(如存款、取款、转账)进行二级分区。
  • 投资分析:金融机构需要对投资数据进行分析,通过分区表按投资产品类型、时间进行分区,可以快速查询某类产品在不同时间段的表现,辅助投资决策。也可以先按交易时间进行一级分区,然后按投资产品类型类型(如股票、债券、基金等)进行二级分区。

优点:

  • 高效数据分析:针对特定账户或时间段的金融数据查询时,减少数据扫描,提高查询速度。
  • 精准风险管理:方便对特定类型或时间段的交易进行细致分析,及时发现并应对风险。

综上列举了部分适合分区表策略的一些应用场景,可以看出分区表在各种应用场景中,都能显著提高数据查询和分析的效率,便于数据管理和维护。通过合理设计分区策略(一级以及二级分区),针对性地进行数据分区,可以有效优化数据库性能,满足不同场景下的业务需求。

总结

分区表和二级分区表技术为大型数据库系统提供了一种高效的数据存储和查询方案。通过灵活定义分区策略和数据划分方式,可以实现数据的按需存储和查询,提高数据处理效率和分析能力。随着数据量的不断增长和业务需求的不断变化,分区表和二级分区表技术将在更多领域得到应用和发展,为数据管理提供更加灵活和高效的解决方案。

点击关注,第一时间了解华为云新鲜技术~

前言

前段时间跟一位前辈聊到前端职业发展该怎么去规划这个问题。他说到的其中几个点我觉得非常好:

  • 第一是要有清晰的自我认知,知道自己在一个团队或者在一个项目中能发挥怎样的价值,不骄傲自大也不要妄自菲薄;
  • 第二点是无论做什么都需要勇于承担,不要害怕任务艰巨自己无法完成,做得越多你收获的也将越多。当然不是指一些机械重复的完成任务,要带着自己的思考;
  • 第三点是任何事情都需要规划,
    任何的能力和价值的养成都是阶段性的积累,在正确的阶段做正确的事
    ,计划对应阶段,每个阶段做每个阶段该做的事,你的提升和成长才是最快的。

第三点我尤为认同,也跟他深入聊了一下关于前端开发的职业生涯分哪些阶段,在每个阶段应该做什么样的努力。

第一个阶段:入门阶段(1-3年)

阶段目标:这个阶段我们的目标就是对纯前端的内容能有一个清晰的认知。能够无障碍找工作,四五次面试能拿到2-3个offer。

入门阶段,就是刚刚入行不久,对前端开发有一点认识,会用基础的前端技术和简单使用前端框架。这时候也能开发业务,但也仅限于开发业务 —— 俗称“页面仔”、cv工程师。这个阶段初期开发的大部分时间在面向搜索引擎。

这个阶段对前端的认识很不全面,技术上既没有广度也没有深度。前端相关的东西都知道一些,但是如果真正深入去聊的话会发现是一知半解。去面试时什么都能说上两句,但是一旦面试官多问两句就会变得支支吾吾。技术栈不够完善和系统,学到的和用到的知识没有整合起来,学的东西大多只是为了当前工作开发去学,工作过程中也只是完成就行,不会想为什么这么做,或者不会想更好的解决方式去完成业务。

那么处于这个阶段该做些什么呢?

  1. 从任何地方汲取前端相关的知识,包括但不限于:视频教程、技术书籍、博客;
  2. 把前端的相关技术和基础详细了解清楚,垂直方向深入。搞清楚前端是做什么的,完成不同的前端工作有哪些方案可以选;
  3. 了解各种框架的本质区别,不要求全部框架都精通。做到
    “一专多熟”
    ,选一个自己常用或者自己感兴趣的框架深入学习,深入一点了解某个框架,做到这个框架无障碍使用,有条件最好能了解一点框架源码;
  4. 了解http协议相关知识、浏览器相关知识。

第二个阶段:全面发展阶段(3-5年)

阶段目标:扩展自己的知识面,对项目结构、项目的周期、研发周期等这些上层的东西有一定了解,慢慢了解接触管理工作。

这个阶段业务已经写得很熟练了,对自己工作的行业有了一定认识,再去同行业得公司工作时,积累的行业知识开始发挥作用,能结合自己对行业的了解和业务去进行开发,也有一定的技术广度和深度,能独立完成项目的构建和一些基本架构,对业务需要用到技术能进行一定的选型。

在这个阶段我们有应该做哪些努力呢?

  1. 扩展知识面,比如:后端、服务器、开发工具等方面的知识;
  2. 学习webpack、docker、Linux等,能够对整个项目的构建过程有一个比较清晰的认识,能从零独立搭建项目;
  3. 提升自己的一些软技能,比如:表达沟通、时间管理等;
  4. 对于自己工作的行业,可以做一些深入了解,了解自己工作的行业中,前端开发有哪些共性和特点,怎么结合行业特点和业务去完成开发工作。

第三个阶段:高级前端开发 / 前端leader(5-7年)

这个阶段你已经属于高级前端开发工程师了,这时候一般有两个方向:

  • 对于喜欢搞技术,不喜欢管理掰扯的人,你是团队中的主程,前端团队中的主力开发,负责团队项目的架构方面的东西,或者一些开发难点的攻坚;
  • 另一个方向就是,团队的管理者,管理一个前端开发团队,负责项目的整体规划和开发进度的把控。

到这个阶段的前端各方面都已经非常成熟了,有完善的前端知识体系,整体项目中,不管是前端部分、后端部分或者一些部署运维的东西,都有比较好的了解,能够同各个环节同事进行对接协调。

那么到了这个阶段,我们还要做些什么呢?

  1. 这个阶段,你应该对整体的前端有比较系统的认知,这时候你工作基本都是管理,管理项目或者团队;
  2. 应该有自己的作品或者产出:不论是开源作品、博客或者插件等都可以;
  3. 收入不单单依靠工作薪资,你对前端开发或者你工作的行业都有自己比较清晰系统的认知,这时候你可能会有自己的相关副业(这个因人而异)。

第四个阶段:前端架构 / 团队领导(7-10年)

到了这个阶段,如果你还在前端行业,那么你要么是一个行业大牛,要么是团队领导。这个阶段的工作方向就是管理和架构方向。当然如果你只是想有份工作,随便做做前端有点收入,保持躺平状态那就不在这些范畴了。

一些建议

  • 学好JavaScript原生的知识。如果你想长期从事前端工作的话,掌握好JavaScript是一切的基础,不管是对于你学习其他技术还是框架来说都是这样;
  • 养成多看源码的习惯。看不同框架、库等的源码可以让我们更多地了解别人是怎么写代码的,学习别人写的思想和方法,这些思想和方法你会发现在很多地方都是通用的;
  • 可以的话参与一些开源项目,或者自己做一些开源的东西,再或者可以保持写博客的习惯,这些都能更好的帮助自己更好的掌握和梳理自己的技术栈;
  • 避免做重复的事,多封装和抽离,不管是方法还是组件。长期保持你后边做项目会非常有效率。

写在后面

那么我们怎么判断自己当前阶段是否做得还不错呢?

  • 去面试,看自己在面试对应阶段的岗位过程中是否顺利,能否比较高比例地拿到offer。

你觉得自己属于哪个阶段?

你觉得每个阶段我们有没有更好的方法去提升自己?

前言

数据库并发,数据审计和软删除一直是数据持久化方面的经典问题。早些时候,这些工作需要手写复杂的SQL或者通过存储过程和触发器实现。手写复杂SQL对软件可维护性构成了相当大的挑战,随着SQL字数的变多,用到的嵌套和复杂语法增加,可读性和可维护性的难度是几何级暴涨。因此如何在实现功能的同时控制这些SQL的复杂度是一个很有价值的问题。而且这个问题同时涉及应用软件和数据库两个相对独立的体系,平行共管也是产生混乱的一大因素。

EF Core作为 .NET平台的高级ORM框架,可以托管和数据库的交互,同时提供了大量扩展点方便自定义。以此为基点把对数据库的操作托管后便可以解决平行共管所产生的混乱,利用LINQ则可以最大程度上降低软件代码的维护难度。

由于项目需要,笔者先后开发并发布了通用的
基于EF Core存储的国际化服务

基于EF Core存储的Serilog持久化服务
,不过这两个功能包并没有深度利用EF Core,虽然主要是因为没什么必要。但是项目还需要提供常用的数据审计和软删除功能,因此对EF Core进行了一些更深入的研究。

起初有考虑过是否使用现成的ABP框架来处理这些功能,但是在其他项目的使用体验来说并不算好,其中充斥着大量上下文依赖的功能,而且这些依赖信息能轻易藏到和最终业务代码相距十万八千里的地方(特别是代码还是别人写的时候),然后在不经意间给你一个大惊喜。对于以代码正交性、非误导性,纯函数化为追求的一介码农(看过我发布的那两个功能包的朋友应该有感觉,一个功能笔者也要根据用途划分为不同的包,确保解决方案中的各个项目都能按需引用,不会残留无用的代码),实在是喜欢不起来ABP这种全家桶。

鉴于项目规模不大,笔者决定针对这些需求做一个专用功能,目标是尽可能减少依赖,方便将来复用到其他项目,降低和其他功能功能冲突的风险。现在笔者将用一系列博客做成果展示。由于这些功能没有经过大范围测试,不确定是否存在未知缺陷,因此暂不打包发布。

新书宣传

有关新书的更多介绍欢迎查看
《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!
image

正文

由于这些功能设计的代码量和知识点较多,为控制篇幅,本文介绍软删除功能。

引言

多年以前就听说过软删除也进行过一些了解,也思考过如何才能优雅地实现,但都因为知识储备不足和需求不紧急而搁置了,这次箭在弦上不得不发了,所幸这些年的积累也为解决这个问题铺平了道路。

关系数据库的一大功能就是确保数据完整性,毕竟关系数据库一开始就是为金融等对数据有严格要求的场景而设计。但是这种功能在某些时候也会带来一些麻烦,例如经典的博客评论关系中,评论一定是要属于某个博客的,这种强依赖会导致如果删除博客,评论的存在意义会同时消失。为保障数据完整性,这些孤儿评论应该同时删除或者至少把外键设置为null以明确表示评论所对应的博客已经不存在。但这种过于强硬的要求会导致没有后悔药可以吃,为了不把事做绝,软删除方案应运而生。通过额外的字段标记数据已被删除,然后在查询时用条件排除此类记录。

看上去一切都很美好,如果不出意外的话就该出意外了。查询语句复杂度陡然上升、唯一约束错乱、级联删除的恢复等让人头痛欲裂的问题接踵而至。为了研究这些问题的解决方案也是翻阅了大量资料,聊的相对深入的一些例如
软删除之痛

在EF Core中使用拦截器实现自动软删除

深入理解 EF Core:使用查询过滤器实现数据软删除
。但是这些资料都没能同时解决这些问题,或者解决方式存在缺陷,当年搁置软删除问题的研究大抵也是出于这些原因。

解决方案

这次借助EF Core的一些功能,总算是用一种比较优雅的方式基本解决这些痛点。

  • 唯一约束错乱:问题的根源在于数据库中的Null值特殊的相等性判定。因此笔者的解决方案是首先禁止删除标记为Null,然后用精确到微秒的时间类型基本解决不可重复删除问题。
  • 软删除的级联和恢复错乱:上文提到的文章使用删除计数来识别记录被删除过几次,但是这种方案对于计数为1的记录依然存在这个1究竟是主动删除导致的还是被级联删除导致的无法准确区分。其次,这种计数方式对事务性的要求较高,需要把主表和可能相关的子表一并锁定避免计数错乱,还可能把删除子表数据的代码蔓延到业务代码中产生大量代码噪音。如果使用触发器还需要解决潜在的递归嵌套溢出和架构迁移维护问题,无论如何都不是个优雅的方案。再次,如果需要直接操作数据库进行数据维护,一旦脑子不清醒忘记更新部分子表的计数器,数据完整性会遭到致命破坏而且很可能无法恢复,因为这种修改很难溯源,无法知道某个计数器更新是何原因也就无法正确回滚。
    笔者的解决方案则是使用独立的删除标记和传递删除标记来实现软删除的自动级联,分离后将不再需要计数器,因此事务问题和架构维护问题自然消失,主动删除和传递删除的恢复问题也一并消失。现在问题也变成了传递删除标记的值要如何产生。对此,笔者的方法是使用视图自动计算,虽然这样可能导致一些查询性能损失,但是这是不可避免的代价,鱼与熊掌不可兼得。随之而来的下一个问题就是视图的查询SQL如何维护,幸好EF Core本身就包含迁移功能来管理数据库架构的变更,那只要把视图定义SQL的维护放到迁移中即可。最后问题就变成如何实现视图定义SQL的自动化维护,否则整个方案都会很难用,而这就是本文的重要内容之一。
  • 查询复杂度陡然上升和已删除数据被意外包含在结果中:查询复杂度的飞速上升主要是因为需要大量增加对传递删除的查询,随着上一个问题的解决,这个问题自然消失,因为这些复杂的查询都被封装在视图定义中了。EF Core新版的表视图多重映射和全局查询过滤器功能更是能在业务层彻底屏蔽软删除的查询问题。而且就算是需要手写SQL查询的地方也可以极大减轻心智负担,因为视图存在于数据库中,就算脱离应用程序依然可以使用。

代码实现

基础接口

/// <summary>
/// 逻辑删除接口
/// </summary>
public interface ILogicallyDeletable
{
    /// <summary>
    /// 逻辑删除标记
    /// </summary>
    /// <remarks>推荐存活的记录使用<see cref="DateTimeOffset.MinValue"/>标记</remarks>
    DateTimeOffset DeletedAt { get; set; }
}

/// <summary>
/// 依赖项逻辑删除接口
/// </summary>
public interface IDependencyLogicallyDeletable
{
    /// <summary>
    /// 依赖项逻辑删除标记
    /// </summary>
    /// <remarks>
    /// <para>用于模拟逻辑删除的级联删除或置Null。</para>
    /// <para>对于必选关系,应该引用主记录的逻辑删除和依赖项逻辑删除标记值中较大的一方,确保级联删除模拟可以自动传播。</para>
    /// <para>推荐使用EF Core的查询视图映射把属性值映射到查询视图并配置全局查询过滤器简化级联逻辑删除的查询。</para>
    /// <para>EF Core实现中表映射到Null值计算列,由查询视图计算实际值。</para>
    /// </remarks>
    DateTimeOffset? DependencyDeletedAt { get; set; }
}

传递删除标记由视图计算,但是EF Core无法配置某个属性只存在于视图中,因此只能使用计算列在表中生成一个同名列以符合EF Core的映射要求。

模型配置扩展

/// <summary>
/// 实体模型配置扩展
/// </summary>
public static class EntityModelBuilderExtensions
{
    internal const string _logicallyDeleteableAnnotationName = "CoreDX.Entity.Property:LogicallyDeleteable";
    internal const string _dependencyLogicallyDeleteableAnnotationName = "CoreDX.Entity.Property:DependencyLogicallyDeleteable";

    internal const string _queryViewAnnotationName = "CoreDX.Relational:QueryView";
    internal const string _queryViewNamePrefixes = "QueryView_";
    internal const string _treeQueryViewNamePrefixes = $"{_queryViewNamePrefixes}Tree_";

    /// <summary>
    /// 使用<see langword="default" />(<see cref="DateTimeOffset.MinValue"/>)
    /// </summary>
    private static readonly DateTimeOffset _aliveEntityTime = default;

    /// <summary>
    /// 配置可逻辑删除实体的查询过滤器让EF Core自动添加查询条件过滤已被逻辑删除的记录。<br/>存活的记录使用<see cref="DateTimeOffset.MinValue"/>标记。
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    /// <returns>实体类型构造器</returns>
    public static EntityTypeBuilder<TEntity> ConfigureQueryFilterForILogicallyDelete<TEntity>(this EntityTypeBuilder<TEntity> builder)
        where TEntity : class, ILogicallyDeletable
    {
        ArgumentNullException.ThrowIfNull(builder);

        // 配置数据库默认值和EF Core哨兵值
        builder.Property(e => e.DeletedAt)
            .HasDefaultValue(_aliveEntityTime)
            .HasSentinel(_aliveEntityTime)
            .HasAnnotation(_logicallyDeleteableAnnotationName, true);

        ConfigQueryViewAnnotationForLogicallyDeletable(builder);

        return builder.HasQueryFilter(e => e.DeletedAt == EF.Constant(_aliveEntityTime));
    }

    /// <summary>
    /// 配置依赖项逻辑删除实体的查询过滤器让EF Core自动添加查询条件过滤已被逻辑删除的记录。
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    /// <param name="nullValueSql">依赖项逻辑删除在表中的计算列Null值生成Sql</param>
    /// <returns>实体类型构造器</returns>
    public static EntityTypeBuilder<TEntity> ConfigureQueryFilterForIDependencyLogicallyDelete<TEntity>(
        this EntityTypeBuilder<TEntity> builder,
        IDependencyLogicallyDeletableNullValueSql nullValueSql)
        where TEntity : class, IDependencyLogicallyDeletable
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(nullValueSql);

        // 配置表的依赖项逻辑删除标记列为值永远为NULL的计算列
        builder.Property(e => e.DependencyDeletedAt)
            .HasComputedColumnSql(nullValueSql.DependencyDeleteAtNullComputedValueSql)
            .HasAnnotation(_dependencyLogicallyDeleteableAnnotationName, true);

        ConfigQueryViewAnnotationForDependencyLogicallyDeletable(builder);

        return builder.HasQueryFilter(e => e.DependencyDeletedAt == null || e.DependencyDeletedAt == EF.Constant(_aliveEntityTime));
    }

    /// <summary>
    /// 配置可逻辑删除和依赖项逻辑删除实体的查询过滤器让EF Core自动添加查询条件过滤已被逻辑删除的记录。
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    /// <param name="nullValueSql">依赖项逻辑删除在表中的计算列Null值生成Sql</param>
    /// <returns>实体类型构造器</returns>
    public static EntityTypeBuilder<TEntity> ConfigureQueryFilterForILogicallyAndIDependencyLogicallyDelete<TEntity>(
        this EntityTypeBuilder<TEntity> builder,
        IDependencyLogicallyDeletableNullValueSql nullValueSql)
        where TEntity : class, ILogicallyDeletable, IDependencyLogicallyDeletable
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(nullValueSql);

        // 配置数据库默认值和EF Core哨兵值
        builder.Property(e => e.DeletedAt)
            .HasDefaultValue(_aliveEntityTime)
            .HasSentinel(_aliveEntityTime)
            .HasAnnotation(_logicallyDeleteableAnnotationName, true);

        // 配置表的依赖项逻辑删除标记列为值永远为NULL的计算列
        builder.Property(e => e.DependencyDeletedAt)
            .HasComputedColumnSql(nullValueSql.DependencyDeleteAtNullComputedValueSql)
            .HasAnnotation(_dependencyLogicallyDeleteableAnnotationName, true);

        ConfigQueryViewAnnotationForLogicallyDeletable(builder);
        ConfigQueryViewAnnotationForDependencyLogicallyDeletable(builder);

        return builder.HasQueryFilter(e => e.DeletedAt == EF.Constant(_aliveEntityTime) && (e.DependencyDeletedAt == null || e.DependencyDeletedAt == EF.Constant(_aliveEntityTime)));
    }

    /// <summary>
    /// 批量配置可逻辑删除和依赖项逻辑删除实体的查询过滤器让EF Core自动添加查询条件过滤已被逻辑删除或传递依赖删除的记录
    /// </summary>
    /// <param name="modelBuilder">模型构造器</param>
    /// <param name="nullValueSql">依赖项逻辑删除在表中的计算列Null值生成Sql</param>
    /// <returns>模型构造器</returns>
    public static ModelBuilder ConfigureQueryFilterForILogicallyDeleteOrIDependencyLogicallyDeletable(
        this ModelBuilder modelBuilder,
        IDependencyLogicallyDeletableNullValueSql nullValueSql)
    {
        ArgumentNullException.ThrowIfNull(modelBuilder);
        ArgumentNullException.ThrowIfNull(nullValueSql);

        foreach (var entity
            in modelBuilder.Model.GetEntityTypes()
                .Where(e => e.ClrType.IsDerivedFrom<ILogicallyDeletable>() || e.ClrType.IsDerivedFrom<IDependencyLogicallyDeletable>()))
        {
            var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);

            var isILogicallyDeletable = entity.ClrType.IsDerivedFrom<ILogicallyDeletable>();
            var isIDependencyLogicallyDeletable = entity.ClrType.IsDerivedFrom<IDependencyLogicallyDeletable>();

            var logicallyDeleteQueryFilterMethod = (isILogicallyDeletable, isIDependencyLogicallyDeletable) switch
            {
                (true, false) => GetEntityTypeConfigurationMethod(
                    nameof(ConfigureQueryFilterForILogicallyDelete),
                    1,
                    entity.ClrType),
                (false, true) => GetEntityTypeConfigurationMethod(
                    nameof(ConfigureQueryFilterForIDependencyLogicallyDelete),
                    2,
                    entity.ClrType),
                (true, true) => GetEntityTypeConfigurationMethod(
                    nameof(ConfigureQueryFilterForILogicallyAndIDependencyLogicallyDelete),
                    2,
                    entity.ClrType),
                // 不可能进入此分支
                (false, false) => throw new InvalidOperationException()
            };

            if (isIDependencyLogicallyDeletable) logicallyDeleteQueryFilterMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null), nullValueSql]);
            else logicallyDeleteQueryFilterMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
        }

        return modelBuilder;
    }

    /// <summary>
    /// 配置实体查询视图。
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    /// <returns>实体类型构造器</returns>
    /// <remarks>
    /// <para>如果实体类型实现<see cref="IDependencyLogicallyDeletable"/>,但不实现<see cref="ITreeEntity{TKey, TEntity}"/>,生成并映射查询到以<see cref="_queryViewNamePrefixes"/>开头的视图。</para>
    /// <para>如果实体类型实现<see cref="ITreeEntity{TKey, TEntity}"/>,但不实现<see cref="IDependencyLogicallyDeletable"/>,生成并映射查询到以<see cref="_treeQueryViewNamePrefixes"/>开头的视图。</para>
    /// <para>如果实体类型同时实现<see cref="IDependencyLogicallyDeletable"/>和<see cref="ITreeEntity{TKey, TEntity}"/>,同时生成以<see cref="_queryViewNamePrefixes"/>和<see cref="_treeQueryViewNamePrefixes"/>开头的视图。<br/>
    /// 实体查询映射到以<see cref="_treeQueryViewNamePrefixes"/>开头的视图,并且<see cref="_treeQueryViewNamePrefixes"/>开头的视图以<see cref="_queryViewNamePrefixes"/>开头的视图为数据源。</para>
    /// </remarks>
    public static EntityTypeBuilder<TEntity> ConfigEntityQueryView<TEntity>(this EntityTypeBuilder<TEntity> builder)
        where TEntity : class
    {
        ArgumentNullException.ThrowIfNull(builder);

        if (builder.Metadata.FindAnnotation(_queryViewAnnotationName)?.Value is List<Type> annotationValue)
        {
            var isDependencyLogicallyDeletableEntity = annotationValue.Any(static x => x == typeof(IDependencyLogicallyDeletable));
            var isTreeEntity = annotationValue.Any(static x => x == typeof(ITreeEntity<,>));

            var tableName = builder.Metadata.GetTableName();
            builder.ToTable(tableName);

            if(isTreeEntity || isDependencyLogicallyDeletableEntity)
            {
                builder.ToView($"{(isTreeEntity ? _treeQueryViewNamePrefixes : _queryViewNamePrefixes)}{tableName}");
            }
        }

        return builder;
    }

    /// <summary>
    /// 批量配置实体查询视图
    /// </summary>
    /// <param name="modelBuilder">模型构造器</param>
    /// <returns>模型构造器</returns>
    /// <remarks>配置规则同<see cref="ConfigEntityQueryView{TEntity}"/></remarks>
    public static ModelBuilder ConfigEntityQueryView(this ModelBuilder modelBuilder)
    {
        ArgumentNullException.ThrowIfNull(modelBuilder);

        foreach (var entity
            in modelBuilder.Model.GetEntityTypes()
                .Where(static e => e.FindAnnotation(_queryViewAnnotationName) is not null))
        {
            var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
            var entityQueryViewMethod = GetEntityTypeConfigurationMethod(
                nameof(ConfigEntityQueryView),
                1,
                entity.ClrType);

            entityQueryViewMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
        }

        return modelBuilder;
    }

    /// <summary>
    /// 配置逻辑删除实体的查询视图注解
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    private static void ConfigQueryViewAnnotationForLogicallyDeletable<TEntity>(EntityTypeBuilder<TEntity> builder)
        where TEntity : class, ILogicallyDeletable
    {
        var annotationValue = builder.Metadata.FindAnnotation(_queryViewAnnotationName)?.Value;
        var typedAnnotationValue = annotationValue as List<Type>;
        if (annotationValue is not null && typedAnnotationValue is null)
        {
            throw new InvalidOperationException($@"模型注解名称""{_queryViewAnnotationName}""已被占用,请把占用此名称的注解修改为其他名称。");
        }
        else if (typedAnnotationValue is null)
        {
            builder.HasAnnotation(_queryViewAnnotationName, new List<Type>() { typeof(ILogicallyDeletable) });
        }
        else if (typedAnnotationValue.Find(static x => x is ILogicallyDeletable) is null)
        {
            typedAnnotationValue.Add(typeof(ILogicallyDeletable));
        }
    }

    /// <summary>
    /// 配置依赖项逻辑删除实体的查询视图注解
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    private static void ConfigQueryViewAnnotationForDependencyLogicallyDeletable<TEntity>(EntityTypeBuilder<TEntity> builder)
        where TEntity : class, IDependencyLogicallyDeletable
    {
        var annotationValue = builder.Metadata.FindAnnotation(_queryViewAnnotationName)?.Value;
        var typedAnnotationValue = annotationValue as List<Type>;
        if (annotationValue is not null && typedAnnotationValue is null)
        {
            throw new InvalidOperationException($@"模型注解名称""{_queryViewAnnotationName}""已被占用,请把占用此名称的注解修改为其他名称。");
        }
        else if (typedAnnotationValue is null)
        {
            builder.HasAnnotation(_queryViewAnnotationName, new List<Type>() { typeof(IDependencyLogicallyDeletable) });
        }
        else if (typedAnnotationValue.Find(static x => x is IDependencyLogicallyDeletable) is null)
        {
            typedAnnotationValue.Add(typeof(IDependencyLogicallyDeletable));
        }
    }
}

SQL模版

相关接口

/// <summary>
/// 依赖项逻辑删除实体的视图列在表中的Null值计算列映射
/// </summary>
public interface IDependencyLogicallyDeletableNullValueSql
{
    string DependencyDeleteAtNullComputedValueSql { get; }
}

public interface ITableOrColumnNameFormattable
{
    /// <summary>
    /// 格式化表或列名
    /// <para>例如为SqlServer列名包裹方括号、MySql包裹反引号</para>
    /// </summary>
    /// <param name="name">表或列名</param>
    /// <returns>格式化的表或列名</returns>
    [return: NotNullIfNotNull(nameof(name))]
    string? FormatTableOrColumnName(string? name);
}

/// <summary>
/// 依赖项逻辑删除实体的视图SQL模板
/// </summary>
public interface IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate : ITableOrColumnNameFormattable
{
    /// <summary>
    /// 创建视图的模板
    /// </summary>
    string CreateSqlTemplate { get; }

    /// <summary>
    /// 删除视图的模板
    /// </summary>
    string DropSqlTemplate { get; }

    /// <summary>
    /// 连接数据源模版
    /// </summary>
    /// <remarks>LEFT JOIN {principalDataSource} ON {tableName}.{foreignKey} = {principalDataSource}.{principalKey}</remarks>
    string JoinTargetTemplate { get; }
    string PrincipalLogicallyDeleteColumnTemplate { get; }
}

接口实现(以SqlServer为例)

public class DefaultSqlServerDependencyLogicallyDeletableNullValueSql : IDependencyLogicallyDeletableNullValueSql
{
    public static DefaultSqlServerDependencyLogicallyDeletableNullValueSql Instance => new();

    private const string _dependencyDeleteAtNullComputedValueSql = "CAST(NULL AS datetimeoffset)";

    public string DependencyDeleteAtNullComputedValueSql => _dependencyDeleteAtNullComputedValueSql;

    private DefaultSqlServerDependencyLogicallyDeletableNullValueSql() { }
}

public class DefaultSqlServerDependencyLogicallyDeletableEntityViewSqlTemplate : IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate
{
    public static DefaultSqlServerDependencyLogicallyDeletableEntityViewSqlTemplate Instance => new();

    private const string _viewNameTemplate = $$"""{{EntityModelBuilderExtensions._queryViewNamePrefixes}}{tableName}""";

    // SqlServer 2022起才支持GREATEST函数
    private const string _createSqlTemplate =
        $$"""
        --创建或重建依赖项逻辑删除实体查询视图
        {{_dropSqlTemplate}}
        CREATE VIEW {{_viewNameTemplate}}
        AS
        SELECT {columns},
            (SELECT MAX([DeleteTimeTable].[DeletedAt])
                FROM (VALUES {principalLogicallyDeleteColumns}) AS DeleteTimeTable([DeletedAt])) AS {dependencyDeletedAtColumn}
        FROM [{tableName}]
        {joinTargets};
        GO
        """;

    private const string _principalLogicallyDeleteColumnTemplate = "({principalLogicallyDeleteColumn})";

    private const string _joinTargetTemplate =
        $$"""
        LEFT JOIN {principalDataSource}
        ON {joinCondition}
        """;

    private const string _dropSqlTemplate =
        $"""
        --删除可能存在的过时依赖项逻辑删除实体查询视图
        IF EXISTS(SELECT * FROM [sysobjects] WHERE [id] = OBJECT_ID(N'{_viewNameTemplate}') AND objectproperty(id, N'IsView') = 1)
        BEGIN
            DROP VIEW [{_viewNameTemplate}]
        END
        GO
        """;

    public string CreateSqlTemplate => _createSqlTemplate;

    public string DropSqlTemplate => _dropSqlTemplate;

    public string PrincipalLogicallyDeleteColumnTemplate => _principalLogicallyDeleteColumnTemplate;

    public string JoinTargetTemplate => _joinTargetTemplate;

    public string? FormatTableOrColumnName(string? name)
    {
        if (name is null) return null;

        return $"[{name}]";
    }

    private DefaultSqlServerDependencyLogicallyDeletableEntityViewSqlTemplate() { }
}

SqlServer 2022才支持取最大值函数,因此这里使用兼容性较高的表值生成函数配合MAX聚合实现取最大值。

数据库迁移扩展

/// <summary>
/// 依赖项逻辑删除实体视图迁移扩展
/// </summary>
public static class DependencyLogicallyDeletableEntityMigrationBuilderExtensions
{
    /// <summary>
    /// 自动扫描迁移模型并配置依赖项逻辑删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="thisVersion">当前版本的迁移</param>
    /// <param name="previousVersion">上一个版本的迁移</param>
    /// <param name="isUp">是否为升级迁移</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder ApplyDependencyLogicallyDeletableEntityQueryView(
        this MigrationBuilder migrationBuilder,
        Migration thisVersion,
        Migration? previousVersion,
        bool isUp,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(thisVersion);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        var thisVersionEntityTypes = thisVersion.TargetModel.GetEntityTypes()
            .Where(static et =>
                (et.FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                ?.Any(x => x == typeof(IDependencyLogicallyDeletable)) is true
            );

        var previousVersionEntityTypes = previousVersion?.TargetModel.GetEntityTypes()
            .Where(static et =>
                (et.FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                ?.Any(x => x == typeof(IDependencyLogicallyDeletable)) is true
            );

        var pendingViewOperations = new List<(IEntityType? entity, string? tableName, bool isCreate)>();

        var tempViewOperationsDict = new Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>>();
        // 表操作
        foreach (var tableOperation in
            migrationBuilder.Operations.Where(static op =>
            {
                var opType = op.GetType();
                return opType.IsDerivedFrom<TableOperation>() || opType.IsDerivedFrom<DropTableOperation>();
            }))
        {
            if (tableOperation is CreateTableOperation createTable)
            {
                // 升级创建表,创建视图
                if (isUp && thisVersionEntityTypes.Any(et => et.GetTableName() == createTable.Name))
                {
                    var entity = thisVersionEntityTypes.Single(en => en.GetTableName() == createTable.Name);
                    AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                }

                // 回滚创建表,说明必然存在上一版迁移,以上一个版本的模型创建视图
                if (!isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes?.Any(et => et.GetTableName() == createTable.Name) is true)
                    {
                        var entity = previousVersionEntityTypes.Single(en => en.GetTableName() == createTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            else if (tableOperation is AlterTableOperation alterTable)
            {
                // 升级修改表(仅当修改表名时),重建视图
                // 因为修改表操作可能是修改表注释
                if (isUp)
                {
                    // 如果上一版本中这个实体是依赖项逻辑删除实体,删除旧视图
                    if (previousVersionEntityTypes?.Any(et => et.GetTableName() == alterTable.OldTable.Name) is true)
                    {
                        // 由于升级修改表名需要同时完成删除基于旧表名的视图和创建基于新表名的视图两个操作
                        // 删除旧视图的操作直接添加到挂起操作列表,修改表名的操作也不会在迁移中重复出现,没有重复添加相同操作的问题
                        pendingViewOperations.Add((null, alterTable.OldTable.Name, false));
                    }

                    if (thisVersionEntityTypes.Any(et => et.GetTableName() == alterTable.Name))
                    {
                        var entity = thisVersionEntityTypes.Single(en => en.GetTableName() == alterTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
                // 回滚修改表(仅当修改表名时),说明必然存在上一版迁移,以上一个版本的模型重建视图
                else
                {
                    // 如果当前版本中这个实体也是依赖项逻辑删除实体,删除旧视图
                    if (thisVersionEntityTypes.Any(et => et.GetTableName() == alterTable.OldTable.Name))
                    {
                        // 由于回滚修改表名需要同时完成删除基于新表名的视图和创建基于旧表名的视图两个操作
                        // 删除旧视图的操作直接添加到挂起操作列表,修改表名的操作也不会在迁移中重复出现,没有重复添加相同操作的问题
                        pendingViewOperations.Add((null, alterTable.OldTable.Name, false));
                    }

                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(et => et.GetTableName() == alterTable.Name) && alterTable.OldTable.Name != alterTable.Name)
                    {
                        var entity = previousVersionEntityTypes!.Single(en => en.GetTableName() == alterTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            // 迁移操作需要删除表,则代表同样需要删除视图
            else if (tableOperation is DropTableOperation dropTable)
            {
                if (isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(en => en.GetTableName() == dropTable.Name))
                    {
                        AddTableDropTableViewToTempDict(tempViewOperationsDict, dropTable.Name);
                    }
                }
                else if (thisVersionEntityTypes.Any(en => en.GetTableName() == dropTable.Name))
                {
                    AddTableDropTableViewToTempDict(tempViewOperationsDict, dropTable.Name);
                }
            }
        }

        // 列操作,每个表可能操作多个列,需要聚合处理
        foreach (var tableColumnOperationsGrouping in
            migrationBuilder.Operations
                .Where(static op =>
                {
                    var opType = op.GetType();
                    return opType.IsDerivedFrom<ColumnOperation>() || opType.IsDerivedFrom<DropColumnOperation>();
                })
                .GroupBy(static op => (op as ColumnOperation)?.Table ?? (op as DropColumnOperation)!.Table))
        {
            foreach (var columnOperation in tableColumnOperationsGrouping)
            {
                if (columnOperation is AddColumnOperation addColumn)
                {
                    // 升级添加列,如果是依赖项逻辑删除,重建视图
                    if (isUp && thisVersionEntityTypes?.Any(et => et.GetTableName() == addColumn.Table) is true)
                    {
                        var entity = thisVersionEntityTypes.Single(en => en.GetTableName() == addColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }

                    // 回滚添加列,说明必然存在上一版迁移,如果上一版是依赖项逻辑删除实体,重建视图
                    if (!isUp)
                    {
                        EnsureMigrationOfPreviousVersion(previousVersion);
                        if (previousVersionEntityTypes?.Any(et => et.GetTableName() == addColumn.Table) is true)
                        {
                            var entity = previousVersionEntityTypes.Single(en => en.GetTableName() == addColumn.Table);
                            AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                        }
                    }
                }
                else if (columnOperation is AlterColumnOperation alterColumn)
                {
                    // 升级修改列,重建视图
                    // 因为修改列操作可能是修改列注释
                    if (isUp)
                    {
                        if (thisVersionEntityTypes!.Any(et => et.GetTableName() == alterColumn.Table))
                        {
                            var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == alterColumn.Table);
                            AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                        }
                        else if (previousVersionEntityTypes?.Any(et => et.GetTableName() == alterColumn.Table) is true)
                        {
                            AddTableDropTableViewToTempDict(tempViewOperationsDict, alterColumn.Table);
                        }
                    }
                    // 回滚修改列(仅当修改列名时),说明必然存在上一版迁移,如果上一版是依赖项逻辑删除实体,重建视图
                    // 因为修改列操作可能是修改列注释
                    else
                    {
                        EnsureMigrationOfPreviousVersion(previousVersion);
                        if (previousVersionEntityTypes?.Any(et => et.GetTableName() == alterColumn.Table) is true)
                        {
                            var entity = previousVersionEntityTypes.Single(en => en.GetTableName() == alterColumn.Table);
                            AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                        }
                        else if (thisVersionEntityTypes!.Any(et => et.GetTableName() == alterColumn.Table))
                        {
                            AddTableDropTableViewToTempDict(tempViewOperationsDict, alterColumn.Table);
                        }
                    }
                }
                else if (columnOperation is DropColumnOperation dropColumn)
                {
                    // 升级删除列
                    if (isUp)
                    {
                        // 当前版本仍然是依赖项逻辑删除实体,说明被删除的列和依赖项逻辑删除无关,重建视图
                        if (thisVersionEntityTypes!.Any(et => et.GetTableName() == dropColumn.Table))
                        {
                            var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == dropColumn.Table);
                            AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                        }
                        // 被删除的列是依赖项逻辑删除(上一版本的实体是依赖项逻辑删除,但当前版本不是),删除视图
                        else if (previousVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                        {
                            AddTableDropTableViewToTempDict(tempViewOperationsDict, dropColumn.Table);
                        }
                    }

                    // 回滚删除列
                    if (!isUp)
                    {
                        EnsureMigrationOfPreviousVersion(previousVersion);
                        // 上一版本是依赖项逻辑删除实体,说明被删除的列和依赖项逻辑删除无关,重建视图
                        if (previousVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                        {
                            var entity = previousVersionEntityTypes.Single(en => en.GetTableName() == dropColumn.Table);
                            AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                        }
                        // 被删除的列是依赖项逻辑删除(上一版本的实体不是依赖项逻辑删除,但当前版本是),删除视图
                        else if (thisVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                        {
                            AddTableDropTableViewToTempDict(tempViewOperationsDict, dropColumn.Table);
                        }
                    }
                }
            }
        }

        // 聚合所有操作,然后选择其中合理的一个作为最终操作
        foreach (var entityViewOperations in tempViewOperationsDict)
        {
            // 理论上来说如果上面的代码没有问题,针对同一张表的操作应该不会出现矛盾
            Debug.Assert(entityViewOperations.Value.All(x => x.isCreate == entityViewOperations.Value.First().isCreate));
            pendingViewOperations.Add(entityViewOperations.Value.First());
        }

        // 检查是依赖项逻辑删除但没有出现在操作列表中的实体
        // 这种实体可能由于依赖的主实体存在迁移操作,导致现有视图过时,需要追加到重建视图的操作列表中
        // 这种情况只会出现在所依赖的主实体从同时是逻辑删除和依赖项逻辑删除实体变成只是其中一种或者从原来是其中一种变成另外一种
        // 或者逻辑删除和依赖项逻辑删除列被改名
        // 主实体的其他列改动与当前实体无关,当前实体的视图对主实体的依赖仅限于主实体的逻辑删除(来自表或视图)和依赖项逻辑删除(一定是视图)(如果主实体也依赖于它的主实体)
        // 主实体从都不是变成至少是其中一种需要在从实体上添加依赖项逻辑删除接口以实现功能,会导致迁移至少会增加一列,因此从实体自身必然会出现在添加列操作中
        // 主实体从至少是其中一种变成都不是,模型构建阶段从实体上的依赖项逻辑删除接口就会抛出异常提示取消对接口的实现,会导致迁移至少会删除一列,因此从实体自身必然会出现在删除列操作中

        // 收集所有添加、删除和改名列操作并按照表分组备用
        var tableColumnOperationsGroupingforTransitiveDependencyCheck = migrationBuilder.Operations
            .Where(static op =>
            {
                var opType = op.GetType();
                return opType.IsDerivedFrom<AddColumnOperation>()
                    // 如果是修改列名,也可能需要重建视图
                    // 在模型上单独配置接口属性的列名映射时需要重建视图
                    // 其他无关列的改名无关紧要,但是因为在这里没有对应的实体属性信息,改名后无法确定哪个是逻辑删除属性对应的列名,只能全部收集后在之后匹配
                    || opType.IsDerivedFrom<AlterColumnOperation>()
                    || opType.IsDerivedFrom<DropColumnOperation>();
            });

        // 收集存在视图操作的实体
        var pendingViewOperationEntities = pendingViewOperations
            .Select(pop =>
                pop.entity ?? (isUp ? thisVersion.TargetModel : previousVersion?.TargetModel)
                ?.GetEntityTypes()
                .SingleOrDefault(et => et.GetTableName()! == (pop.tableName ?? pop.entity!.GetTableName())))
            .Where(static pop => pop is not null)
            .ToList();

        // 收集自身不在操作列表中,但依赖的主实体在操作列表中的实体
        // 升级迁移收集当前版本的迁移实体,回滚迁移收集上一版本的迁移实体
        var principalInPendingViewOperationEntities = (isUp ? thisVersionEntityTypes : previousVersionEntityTypes)
            ?.Where(et => !pendingViewOperationEntities.Contains(et))
            .Where(et =>
                et.GetForeignKeys()
                    .Select(static fk => fk.PrincipalEntityType)
                    .Any(pet => pendingViewOperationEntities.Contains(pet))
            )
            .ToList() ?? [];

        // 把这些实体加入视图重建列表
        pendingViewOperations.AddRange(principalInPendingViewOperationEntities?.Select(et => ((IEntityType?)et, (string?)null, true)) ?? []);

        foreach (var (entity, tableName, _) in pendingViewOperations.Where(op => !op.isCreate))
        {
            if (entity is not null) migrationBuilder.DropDependencyLogicallyDeletableEntityQueryView(entity, sqlTemplate);
            else if (tableName is not null) migrationBuilder.DropDependencyLogicallyDeletableEntityQueryView(tableName, sqlTemplate);
            else throw new InvalidOperationException("迁移实体类型和迁移表名不能同时为 null。");
        }

        migrationBuilder.CreateDependencyLogicallyDeletableEntityQueryViewsByDataSourceDependency(
            pendingViewOperations.Where(op => op.isCreate).Select(op => op.entity!),
            sqlTemplate);

        return migrationBuilder;

        /// <summary>
        /// 确保提供了上一版本的迁移
        /// </summary>
        static void EnsureMigrationOfPreviousVersion(Migration? previousVersion)
        {
            if (previousVersion is null) throw new InvalidOperationException($"回滚操作指出存在更早版本的迁移,但未提供上一版本的迁移。");
        }

        /// <summary>
        /// 向按表分组的临时操作存放字典添加创建实体视图命令
        /// </summary>
        static void AddEntityCreateEntityViewToTempDict(Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>> tempViewOperationsDict, IEntityType entity)
        {
            if (!tempViewOperationsDict.TryGetValue(entity.GetTableName()!, out var result))
            {
                result ??= [];
                tempViewOperationsDict.Add(entity.GetTableName()!, result);
            }
            result.Add((entity, null, true));
        }

        /// <summary>
        /// 向按表分组的临时操作存放字典添加删除表视图命令
        /// </summary>
        static void AddTableDropTableViewToTempDict(Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>> tempViewOperationsDict, string tableName)
        {
            if (!tempViewOperationsDict.TryGetValue(tableName, out var result))
            {
                result ??= [];
                tempViewOperationsDict.Add(tableName, result);
            }
            result.Add((null, tableName, false));
        }
    }

    /// <summary>
    /// 创建依赖项逻辑删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder CreateDependencyLogicallyDeletableEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        var (sql, _, _) = CreateDependencyLogicallyDeletableEntityQueryViewSql(entityType, sqlTemplate);
        migrationBuilder.Sql(sql);
        return migrationBuilder;
    }

    /// <summary>
    /// 创建依赖项逻辑删除视图并对视图创建进行排序,确保被依赖的主实体视图优先创建
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityTypes">实体类型集合</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder CreateDependencyLogicallyDeletableEntityQueryViewsByDataSourceDependency(
        this MigrationBuilder migrationBuilder,
        IEnumerable<IEntityType> entityTypes,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        var viewSqls = entityTypes
            .Select(e => CreateDependencyLogicallyDeletableEntityQueryViewSql(e, sqlTemplate))
            .ToImmutableList();

        var viewNames = viewSqls
            .Select(vs => vs.viewName)
            .ToImmutableList();
        var rootViewSqls = viewSqls.Where(x =>
            x.principalDataSourceNames
                .All(ds => !ds.isViewDependency || !viewNames.Contains(ds.principalDataSourceName)));
        var viewSqlTrees = rootViewSqls.Select(rv =>
            rv.AsHierarchical(v =>
                viewSqls.Where(vs =>
                    vs.principalDataSourceNames
                        .Select(static dsn => dsn.principalDataSourceName)
                        .Contains(v.viewName))));
        var orderedViewSqls = viewSqlTrees
            .SelectMany(tr => tr.AsEnumerable())
            .GroupBy(h => h.Current.viewName)
            .Select(hg => hg.OrderByDescending(h => h.Level).First())
            .OrderBy(h => h.Level)
            .Select(h => h.Current);

        foreach (var (sql, _, _) in orderedViewSqls) migrationBuilder.Sql(sql);

        return migrationBuilder;
    }

    /// <summary>
    /// 删除依赖项逻辑删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropDependencyLogicallyDeletableEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        return migrationBuilder.DropDependencyLogicallyDeletableEntityQueryView(entityType.GetTableName()!, sqlTemplate);
    }

    /// <summary>
    /// 删除依赖项逻辑删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="tableName">视图对应的表名</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropDependencyLogicallyDeletableEntityQueryView(
        this MigrationBuilder migrationBuilder,
        string tableName,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(sqlTemplate);
        if (string.IsNullOrEmpty(tableName))
        {
            throw new ArgumentException($"“{nameof(tableName)}”不能为 null 或空。", nameof(tableName));
        }

        migrationBuilder.Sql(sqlTemplate.DropSqlTemplate.Replace("{tableName}", tableName));

        return migrationBuilder;
    }

    /// <summary>
    /// 创建依赖项逻辑删除视图
    /// </summary>
    /// <param name="entityType">实体类型</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>视图创建Sql、视图名称、依赖数据源名称以及依赖类型的集合,为视图创建排序提供线索</returns>
    private static (string sql, string viewName, IReadOnlyList<(string principalDataSourceName, bool isViewDependency)> principalDataSourceNames) CreateDependencyLogicallyDeletableEntityQueryViewSql(
        IEntityType entityType,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        var isDependencyLogicallyDeletableEntity = (entityType
            .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
            ?.Any(static x => x == typeof(IDependencyLogicallyDeletable)) is true;

        if (!isDependencyLogicallyDeletableEntity) throw new InvalidOperationException($"{entityType.Name}不是依赖项逻辑删除实体或未配置视图生成。");

        var tableName = entityType.GetTableName()!;
        var formatTableName = sqlTemplate.FormatTableOrColumnName(tableName)!;

        var tableIdentifier = StoreObjectIdentifier.Table(tableName)!;
        var columnEnumerable = entityType.GetProperties()
            .Where(static prop => prop.FindAnnotation(EntityModelBuilderExtensions._dependencyLogicallyDeleteableAnnotationName)?.Value is not true)
            .Select(prop => $"{formatTableName}.{sqlTemplate.FormatTableOrColumnName(prop.GetColumnName(tableIdentifier))}");
        var columns = string.Join(", ", columnEnumerable);

        var foreignKeys = entityType.GetForeignKeys()
            .Where(static fk => fk.DeleteBehavior is DeleteBehavior.Cascade or DeleteBehavior.SetNull)
            .Where(static fk =>
            {
                var annotationValue = fk.PrincipalEntityType
                    .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>;
                return annotationValue
                    ?.Any(static t =>
                        t == typeof(ILogicallyDeletable) || t == typeof(IDependencyLogicallyDeletable)) is true;
            })
            .Select(fk => BuildJoinInfo(entityType, fk))
            .ToList();

        if (foreignKeys.Count == 0) throw new InvalidOperationException($"实体{entityType.Name}没有支持逻辑删除或依赖项逻辑删除的级联外键,无需实现{nameof(IDependencyLogicallyDeletable)}接口。");

        var principalLogicallyDeleteColumns = string.Join(
            ", ",
            foreignKeys
                .SelectMany(p => BuildPrincipalDataSources(p.Key))
                .Select(x => sqlTemplate.PrincipalLogicallyDeleteColumnTemplate.Replace("{principalLogicallyDeleteColumn}", x))
        );

        var joinTargetEnumerable = foreignKeys.Select(p =>
        {
            var formatPrincipalDataSourceName = sqlTemplate.FormatTableOrColumnName(p.Key.principalDataSourceName)!;
            var conditions = p.Value.Select(x => $"{formatTableName}.{sqlTemplate.FormatTableOrColumnName(x.foreignKeyName)} = {formatPrincipalDataSourceName}.{sqlTemplate.FormatTableOrColumnName(x.principalKeyName)}");
            return sqlTemplate.JoinTargetTemplate
                .Replace("{principalDataSource}", formatPrincipalDataSourceName)
                .Replace("{joinCondition}", string.Join(" AND ", conditions));
        });

        var joinTargets = string.Join("\r\n", joinTargetEnumerable);

        var dependencyDeletedAtProperty = entityType.GetProperties()
            .Single(static prop => prop.FindAnnotation(EntityModelBuilderExtensions._dependencyLogicallyDeleteableAnnotationName)?.Value is true);

        var sql = sqlTemplate.CreateSqlTemplate
            .Replace("{tableName}", tableName)
            .Replace("{columns}", columns)
            .Replace("{dependencyDeletedAtColumn}", sqlTemplate.FormatTableOrColumnName(dependencyDeletedAtProperty.GetColumnName(tableIdentifier)))
            .Replace("{principalLogicallyDeleteColumns}", principalLogicallyDeleteColumns)
            .Replace("{joinTargets}", joinTargets);

        return (sql, entityType.GetViewName()!, foreignKeys.Select(x => (x.Key.principalDataSourceName, x.Key.isViewDependency)).ToImmutableList());

        static KeyValuePair<(string principalDataSourceName, string? principalLogicallyDeletableColumnName, string? principalDependencyLogicallyDeletableColumnName, bool isViewDependency), IReadOnlyList<(string foreignKeyName, string principalKeyName)>> BuildJoinInfo(IEntityType entityType, IForeignKey foreignKey)
        {
            if (foreignKey.Properties.Count != foreignKey.PrincipalKey.Properties.Count) throw new InvalidOperationException($"外键和主键字段数量不一致。外键实体:{entityType.Name};主实体:{foreignKey.PrincipalEntityType.Name}");

            var principalEntityType = foreignKey.PrincipalEntityType;

            var principalIsDependencyLogicallyDeletableEntity = (principalEntityType
                .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                ?.Any(static t => t == typeof(IDependencyLogicallyDeletable)) is true;

            var principalDataSourceName = principalIsDependencyLogicallyDeletableEntity
                ? principalEntityType.GetViewName()!
                : principalEntityType.GetTableName()!;
            var principalDataSourceIdentifier = principalIsDependencyLogicallyDeletableEntity
                ? StoreObjectIdentifier.View(principalDataSourceName)
                : StoreObjectIdentifier.Table(principalDataSourceName);

            var foreignTableName = entityType.GetTableName()!;
            var foreignTableIdentifier = StoreObjectIdentifier.Table(foreignTableName);
            var foreignKeyNames = foreignKey.Properties.Select(prop => prop.GetColumnName(foreignTableIdentifier)!);
            var principalKeyNames = foreignKey.PrincipalKey.Properties.Select(prop => prop.GetColumnName(principalDataSourceIdentifier)!);

            var joinConditionPairs = foreignKeyNames.Zip(principalKeyNames, (fk, pk) => (foreignKeyName: fk, principalKeyName: pk));

            return KeyValuePair.Create(
                (
                    principalDataSourceName,
                    principalEntityType.GetProperties()
                        .SingleOrDefault(prop => prop.FindAnnotation(EntityModelBuilderExtensions._logicallyDeleteableAnnotationName)?.Value is true)
                        ?.GetColumnName(principalDataSourceIdentifier),
                    principalEntityType.GetProperties()
                        .SingleOrDefault(prop => prop.FindAnnotation(EntityModelBuilderExtensions._dependencyLogicallyDeleteableAnnotationName)?.Value is true)
                        ?.GetColumnName(principalDataSourceIdentifier),
                    principalIsDependencyLogicallyDeletableEntity
                ),
                joinConditionPairs.ToImmutableList() as IReadOnlyList<(string, string)>
            );
        }

        IEnumerable<string> BuildPrincipalDataSources((string principalDataSourceName, string? principalLogicallyDeletableColumnName, string? principalDependencyLogicallyDeletableColumnName, bool _) val)
        {
            if (val.principalLogicallyDeletableColumnName is not null)
                yield return $"{sqlTemplate.FormatTableOrColumnName(val.principalDataSourceName)}.{sqlTemplate.FormatTableOrColumnName(val.principalLogicallyDeletableColumnName)}";
            if (val.principalDependencyLogicallyDeletableColumnName is not null)
                yield return $"{sqlTemplate.FormatTableOrColumnName(val.principalDataSourceName)}.{sqlTemplate.FormatTableOrColumnName(val.principalDependencyLogicallyDeletableColumnName)}";
        }
    }
}

迁移扩展中对视图操作的排序用到了树形结构,感兴趣的朋友可以查看笔者的早期博客
C# 通用树形数据结构
了解详细信息。

数据库上下文

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : ApplicationIdentityDbContext<
        ApplicationUser,
        ApplicationRole,
        IdentityKey,
        ApplicationUserClaim,
        ApplicationUserRole,
        ApplicationUserLogin,
        ApplicationRoleClaim,
        ApplicationUserToken>(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 其他无关代码

        // 自动根据数据库类型进行数据库相关的模型配置
        switch (Database.ProviderName)
        {
            case _msSqlServerProvider:
                modelBuilder.ApplyConfigurationsFromAssembly(
                    typeof(LogRecordEntityTypeConfiguration).Assembly,
                    type => type.GetCustomAttributes<DatabaseProviderAttribute>().Any(a => a.ProviderName is _msSqlServerProvider));

                modelBuilder.ConfigureQueryFilterForILogicallyDeleteOrIDependencyLogicallyDeletable(DefaultSqlServerDependencyLogicallyDeletableNullValueSql.Instance);
                break;
            case _pomeloMySqlProvider:
                modelBuilder.ApplyConfigurationsFromAssembly(
                    typeof(LogRecordEntityTypeConfiguration).Assembly,
                    type => type.GetCustomAttributes<DatabaseProviderAttribute>().Any(a => a.ProviderName is _pomeloMySqlProvider));

                modelBuilder.ConfigureForTimeAuditable(DefaultMySqlTimeAuditableDefaultValueSql.Instance);
                modelBuilder.ConfigureQueryFilterForILogicallyDeleteOrIDependencyLogicallyDeletable(DefaultMySqlDependencyLogicallyDeletableNullValueSql.Instance);
                break;
            case _msSqliteProvider:
                goto default;
            default:
                throw new NotSupportedException(Database.ProviderName);
        }

        modelBuilder.ConfigEntityQueryView();
    }
}

拦截器

/// <summary>
/// 把逻辑删除实体的删除变更为编辑,设置删除时间,然后使用<see cref="LogicallyDeletedRuntimeAnnotation"/>标记运行时注释便于区分普通的已编辑实体
/// </summary>
public class LogicallyDeletableSaveChangesInterceptor : SaveChangesInterceptor
{
    /// <summary>
    /// 逻辑删除实体的运行时注释名,注释的值为<see langword="true"/>
    /// </summary>
    public const string LogicallyDeletedRuntimeAnnotation = "Runtime:LogicallyDeleted";

    protected IServiceScopeFactory ScopeFactory { get; }

    public LogicallyDeletableSaveChangesInterceptor(IServiceScopeFactory scopeFactory)
    {
        ArgumentNullException.ThrowIfNull(scopeFactory);

        ScopeFactory = scopeFactory;
    }

    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        OnSavingChanges(eventData);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        OnSavingChanges(eventData);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
    {
        OnSavedChanges(eventData);
        return base.SavedChanges(eventData, result);
    }

    public override ValueTask<int> SavedChangesAsync(
        SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        OnSavedChanges(eventData);
        return base.SavedChangesAsync(eventData, result, cancellationToken);
    }

    public override void SaveChangesCanceled(DbContextEventData eventData)
    {
        OnSaveChangesCanceled(eventData);
        base.SaveChangesCanceled(eventData);
    }

    public override Task SaveChangesCanceledAsync(
        DbContextEventData eventData,
        CancellationToken cancellationToken = default)
    {
        OnSaveChangesCanceled(eventData);
        return base.SaveChangesCanceledAsync(eventData, cancellationToken);
    }

    public override void SaveChangesFailed(DbContextErrorEventData eventData)
    {
        OnSaveChangesFailed(eventData);
        base.SaveChangesFailed(eventData);
    }

    public override Task SaveChangesFailedAsync(
        DbContextErrorEventData eventData,
        CancellationToken cancellationToken = default)
    {
        OnSaveChangesFailed(eventData);
        return base.SaveChangesFailedAsync(eventData, cancellationToken);
    }

    protected virtual void OnSavingChanges(DbContextEventData eventData)
    {
        ArgumentNullException.ThrowIfNull(eventData.Context);

        using var scope = ScopeFactory.CreateScope();
        var timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>();

        eventData.Context.ChangeTracker.DetectChanges();

        // 按实体元数据分组
        var typedEntries = eventData.Context.ChangeTracker.Entries()
            .Where(static entry => entry.State is EntityState.Deleted)
            .GroupBy(static entry => entry.Metadata);

        foreach (var entries in typedEntries)
        {
            // 相同元数据的不同上下文对象使用不同的逻辑删除缓存
            // 实体模型的运行时元数据会通过逻辑删除缓存持有上下文对象的引用,需要在所有保存拦截方法中处理缓存并删除引用
            var logicalDeletedCacheDictionary = entries.Key
                .GetOrAddRuntimeAnnotationValue<ConcurrentDictionary<DbContext, HashSet<EntityEntry>>, object?>(
                    LogicallyDeletedRuntimeAnnotation,
                    static dbContext => [],
                    null);
            var logicalDeletedCache = logicalDeletedCacheDictionary.GetOrAdd(eventData.Context, []);

            foreach (var entry in entries)
            {
                if (entry.Entity is ILogicallyDeletable logicallyDeletable)
                {
                    entry.State = EntityState.Modified;
                    logicallyDeletable.DeletedAt = timeProvider.GetLocalNow();

                    // 使用运行时注释缓存逻辑删除的实体
                    logicalDeletedCache.Add(entry);
                }
            }
        }
    }

    protected virtual void OnSavedChanges(SaveChangesCompletedEventData eventData)
    {
        // 保存成功时需要解除对实体的跟踪,其他情况无需处理
        PostProcessEntriesAndCleanLogicalDeletedCache(eventData, static entry => entry.State = EntityState.Detached);
    }

    protected virtual void OnSaveChangesCanceled(DbContextEventData eventData)
    {
        PostProcessEntriesAndCleanLogicalDeletedCache(eventData);
    }

    protected virtual void OnSaveChangesFailed(DbContextEventData eventData)
    {
        PostProcessEntriesAndCleanLogicalDeletedCache(eventData);
    }

    /// <summary>
    /// 获取已逻辑删除的实体
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="eventData"></param>
    /// <returns>已逻辑删除的实体集合</returns>
    protected static IReadOnlyList<EntityEntry> GetLogicallyDeletedEntries<TEntity>(DbContextEventData eventData)
        where TEntity : class
    {
        var dict = eventData.Context?.Model
            .FindEntityType(typeof(TEntity))
            ?.FindRuntimeAnnotationValue(LogicallyDeletedRuntimeAnnotation) as ConcurrentDictionary<DbContext, HashSet<EntityEntry>>;

        var entities = dict
            ?.GetValueOrDefault(eventData.Context!)
            ?.Where(static e => e.Entity is TEntity)
            ?.ToImmutableArray() ?? [];

        return entities;
    }

    /// <summary>
    /// 获取已逻辑删除的实体
    /// </summary>
    /// <param name="eventData"></param>
    /// <returns>已逻辑删除的实体集合</returns>
    protected static IReadOnlyList<EntityEntry> GetLogicallyDeletedEntries(DbContextEventData eventData)
    {
        var entities = eventData.Context?.Model
            .GetEntityTypes()
            .Select(static et => et.FindRuntimeAnnotationValue(LogicallyDeletedRuntimeAnnotation) as ConcurrentDictionary<DbContext, HashSet<EntityEntry>>)
            .Where(static dict => dict is not null)
            .Select(dict => dict!.GetValueOrDefault(eventData.Context!))
            .Where(static hs => hs is { Count: > 0 })
            .SelectMany(static hs => hs!)
            .ToImmutableArray() ?? [];

        return entities;
    }

    /// <summary>
    /// 在保存后事件调用,执行自定义实体处理,然后清除运行时元数据注释的缓存,避免内存泄漏
    /// </summary>
    /// <param name="eventData">事件数据</param>
    /// <param name="action">自定义处理委托</param>
    protected static void PostProcessEntriesAndCleanLogicalDeletedCache(DbContextEventData eventData, Action<EntityEntry>? action = null)
    {
        ArgumentNullException.ThrowIfNull(eventData.Context);

        var entrySetDict = eventData.Context.ChangeTracker.Entries()
            .GroupBy(static e => e.Metadata)
            .Select(static group => group.Key.FindRuntimeAnnotationValue(LogicallyDeletedRuntimeAnnotation) as ConcurrentDictionary<DbContext, HashSet<EntityEntry>>)
            .Where(static dict => dict is not null)
            .ToList();

        var entrySets = entrySetDict
            .Select(dict => dict!.GetValueOrDefault(eventData.Context))
            .Where(static set => set is not null);

        foreach (var set in entrySets)
        {
            foreach (var entry in set!)
            {
                action?.Invoke(entry);
            }

            set.Clear();
        }

        // 清空当前上下文的逻辑删除缓存避免内存泄漏
        foreach (var dict in entrySetDict)
        {
            dict!.TryRemove(eventData.Context, out var _);
        }
    }
}

/// <summary><inheritdoc cref="LogicallyDeletableSaveChangesInterceptor"/></summary>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TKey">用户实体主键类型</typeparam>
/// <param name="scopeFactory"></param>
/// <remarks>为 Identity 实体的唯一索引属性设置特别删除标记</remarks>
public class IdentityLogicallyDeletableSaveChangesInterceptor<TUser, TKey>(IServiceScopeFactory scopeFactory)
    : LogicallyDeletableSaveChangesInterceptor(scopeFactory)
    where TUser : IdentityUser<TKey>, ILogicallyDeletable
    where TKey : IEquatable<TKey>
{
    private const string _delMark = "!del";

    /// <summary>
    /// 把已删除的用户的特殊属性增加标记,避免无法创建同名用户和查询出已删除的同名用户
    /// </summary>
    /// <param name="eventData"></param>
    protected override void OnSavingChanges(DbContextEventData eventData)
    {
        // 此处会把应该逻辑删除的已删除实体调整为已修改,不能通过ChangeTracker找到已逻辑删除的实体
        base.OnSavingChanges(eventData);

        var entityEntries = GetLogicallyDeletedEntries<TUser>(eventData);

        foreach (var entry in entityEntries)
        {
            var entity = entry.Entity as TUser;

            entity!.Email += _delMark + entity.DeletedAt.Ticks;
            entity.NormalizedEmail += _delMark.ToUpperInvariant() + entity.DeletedAt.Ticks;
            entity.UserName += _delMark + entity.DeletedAt.Ticks;
            entity.NormalizedUserName += _delMark.ToUpperInvariant() + entity.DeletedAt.Ticks;
        }
    }
}

服务配置

services.AddPooledDbContextFactory<ApplicationDbContext>((sp, options) =>
{
    if (sp.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
    {
        options.EnableSensitiveDataLogging();
        options.EnableDetailedErrors();
    }

    // 注册拦截器
    var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
    options.AddInterceptors(new IdentityLogicallyDeletableSaveChangesInterceptor<ApplicationUser, IdentityKey>(scopeFactory));

    ConfigureDbConnection(options, sp);
});

迁移

/// <inheritdoc />
public partial class V0002 : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 其他迁移工具生成的代码

        migrationBuilder.ApplyEntityQueryView(
            this, // 当前迁移
            new V0001() { ActiveProvider = this.ActiveProvider }, // 上一个迁移
            true, // 是升级迁移
            DefaultSqlServerDependencyLogicallyDeletableEntityViewSqlTemplate.Instance,
            DefaultSqlServerTreeEntityViewSqlTemplate.Instance); // 下篇会详细说明
    }

    /// <inheritdoc />
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 其他迁移工具生成的代码

        migrationBuilder.ApplyEntityQueryView(
            this,
            new V0001() { ActiveProvider = this.ActiveProvider },
            false, // 是回滚迁移
            DefaultSqlServerDependencyLogicallyDeletableEntityViewSqlTemplate.Instance,
            DefaultSqlServerTreeEntityViewSqlTemplate.Instance);
    }
}

此处以第二版迁移为例,方便演示回滚迁移所需的参数。只需要调用这一个扩展方法就可以完成视图的自动迁移,生成的迁移脚本也是完整的,可以直接使用。

唯一的遗憾是模型差异比较需要在迁移处现场完成,为此需要手动提供上一个迁移模型。如果要完全在框架中完成比较和代码生成,需要改动的东西太多也很麻烦,权衡之后决定自行在迁移中实现。如果哪天EF Core把这方面的功能做成类似拦截器那种不需要继承现有类型就能插入迁移代码生成流程的话能方便不少。

由于EF Core支持很多复杂的模型映射,迁移扩展可能还有没覆盖到的情况,目前只能说常规映射基本正常。

这个实现套路也是参考了笔者之前的一篇旧文
EntityFramework Core 2.x/3.x (ef core) 在迁移中自动生成数据库表和列说明
。不过现在注释已经是内置迁移功能的一部分,因此无需手动实现。参考项目中保留了相关代码,只不过把实现方案换成了挂接到内置实现,到模型构造的部分依然不变,但不再需要手动调整迁移代码,变成真正的一次编写永久使用。

迁移脚本预览(节选)

-- 同时依赖多个主实体的情况
CREATE VIEW QueryView_Entity2_1s
AS
SELECT [Entity2_1s].[Id], [Entity2_1s].[DeletedAt], [Entity2_1s].[Depth], [Entity2_1s].[Entity1_1_1Id], [Entity2_1s].[Entity2Id], [Entity2_1s].[Entity2_0Id], [Entity2_1s].[HasChildren], [Entity2_1s].[IsLeaf], [Entity2_1s].[IsRoot], [Entity2_1s].[ParentId], [Entity2_1s].[Path], [Entity2_1s].[Text2_1],
    (SELECT MAX([DeleteTimeTable].[DeletedAt])
        FROM (VALUES ([Entity1_1_1s].[DeletedAt]), ([Entity2_0s].[DeletedAt])) AS DeleteTimeTable([DeletedAt])) AS [DependencyDeletedAt]
FROM [Entity2_1s]
LEFT JOIN [Entity1_1_1s]
ON [Entity2_1s].[Entity1_1_1Id] = [Entity1_1_1s].[Id]
LEFT JOIN [Entity2_0s]
ON [Entity2_1s].[Entity2_0Id] = [Entity2_0s].[Id];
GO

IF EXISTS(SELECT * FROM [sysobjects] WHERE [id] = OBJECT_ID(N'QueryView_Entity3s') AND objectproperty(id, N'IsView') = 1)
BEGIN
    DROP VIEW [QueryView_Entity3s]
END
GO

-- 依赖的主实体也有视图的情况
CREATE VIEW QueryView_Entity3s
AS
SELECT [Entity3s].[Id], [Entity3s].[Entity1_1Id], [Entity3s].[Text2],
    (SELECT MAX([DeleteTimeTable].[DeletedAt])
        FROM (VALUES ([QueryView_Entity1_1s].[DeletedAt]), ([QueryView_Entity1_1s].[MyDependencyDeletedAt])) AS DeleteTimeTable([DeletedAt])) AS [DependencyDeletedAt]
FROM [Entity3s]
LEFT JOIN [QueryView_Entity1_1s]
ON [Entity3s].[Entity1_1Id] = [QueryView_Entity1_1s].[Id];
GO

外键和主实体是复合列的情况也是支持的,只是在示例中没有用上,之前单独的开发测试时验证过,如果不出意外应该是不会坏掉。

结语

经过这一系列的操作,最终实现了软删除和级联软删除的自动化且能有效模拟级联删除的行为,在数据库端也能尽可能简化查询的编写。利用EF Core的各种高级功能最终实现了对业务代码的0侵入。

示例代码:
SoftDeleteDemo.rar
。主页显示异常请在libman.json上右键恢复前端包。

QQ群

读者交流QQ群:540719365
image

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。

本文地址:
https://www.cnblogs.com/coredx/p/18305274.html

线上的程序跑着跑着内存越来越大,并且没有下降的趋势,重启一下程序也只能短暂恢复。通过
htop
命令再按一下
M
键按内存占用大小排个序,程序会占好几个G。那好,让我们来分析一下。

收集dump

通过
top

htop
进程管理器,或
ps
命令查找到目标进程 id,然后使用如下命令生成 dump:

createdump --full <PID>

dump 会自动保存在
/tmp
目录,文件名格式一般是
coredump.PID

小试牛刀

拿到 dump 后,通过 windbg 打开。首先使用命令
!address -summary
查看一下。

注意:有的朋友可能打开 dump 文件后,发现 sos 扩展加载失败,需要手动加载一下,输入命令
.load sos
即可。

0:000> !address -summary

                                     
Mapping file section regions...
Mapping module regions...
Mapping heap regions...
*** WARNING: Unable to verify timestamp for libc-2.17.so

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown>                               869 ffffffff`f409b600 (  16.000 EB) 100.00%  100.00%
Image                                  1117        0`0b565a00 ( 181.397 MB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
                                        808 ffffffff`018de000 (  16.000 EB)          100.00%
MEM_PRIVATE                            1178        0`fdd23000 (   3.966 GB)   0.00%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
                                        808 ffffffff`018de000 (  16.000 EB) 100.00%  100.00%
MEM_COMMIT                             1178        0`fdd23000 (   3.966 GB)   0.00%    0.00%

--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READWRITE                          520        0`f6bf0000 (   3.855 GB)   0.00%    0.00%
PAGE_EXECUTE_READ                       160        0`05141000 (  81.254 MB)   0.00%    0.00%
PAGE_READONLY                           466        0`0147c000 (  20.484 MB)   0.00%    0.00%
PAGE_EXECUTE_WRITECOPY                   32        0`00b76000 (  11.461 MB)   0.00%    0.00%

--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
<unknown>                              7ffd`7f5ad000 ffff8002`80053000 (  16.000 EB)
Image                                  7f44`7390c000        0`013d2000 (  19.820 MB)

从输出结果中
MEM_COMMIT
一栏可以看出,内存占了3.9G。

下一步要看是托管内存还是非托管内存吃掉。

托管内存

使用
eeheap
命令查看:

0:000> !sos eeheap 
Loader Heap:
----------------------------------------
System Domain:        7f447979a8c0
LoaderAllocator:      7f447979a8c0
LowFrequencyHeap:     7f4406ce0000(10000:c000) 7f4406cc0000(10000:10000) 7f4406ca0000(10000:10000) 7f4406c80000(10000:10000) 7f4406c50000(10000:10000) 7f4406c20000(10000:10000) 7f4406c00000(10000:10000) 7f4406be0000(10000:10000) 7f4406bc0000(10000:10000) 7f4406b60000(10000:10000) 7f4406b30000(10000:10000) 7f4406b00000(10000:10000) 7f4406ad0000(10000:10000) 7f4406a90000(20000:20000) 7f4406a70000(10000:a000) 7f4406a20000(10000:f000) 7f4406a00000(10000:10000) 7f44069e0000(10000:10000) 7f44069d0000(10000:10000) 7f4406990000(40000:40000) 7f4406950000(30000:29000) 7f4406940000(10000:10000) 7f4406720000(10000:f000) 7f4406700000(10000:10000) 7f44066e0000(10000:10000) 7f44066d0000(10000:10000) 7f44066b0000(10000:10000) 7f4406690000(10000:10000) 7f4406670000(10000:10000) 7f4406540000(10000:10000) 7f4406530000(10000:10000) 7f4406390000(10000:b000) 7f4406330000(10000:10000) 7f4406300000(10000:10000) 7f44062e0000(10000:10000) 7f4406270000(10000:10000) 7f4406240000(10000:10000) 7f4406210000(10000:10000) 7f4406200000(10000:10000) 7f44061d0000(30000:2a000) 7f44061b0000(10000:4000) 7f4406190000(10000:10000) 7f4406170000(10000:10000) 7f4406160000(10000:f000) 7f4406140000(10000:c000) 7f4406110000(10000:10000) 7f44060e0000(10000:10000) 7f4405ed0000(10000:10000) 7f4405eb0000(10000:10000) 7f4405e90000(10000:10000) 7f4405e70000(10000:10000) 7f4405e50000(10000:f000) 7f4405d70000(10000:10000) 7f4405d50000(10000:10000) 7f4405d20000(10000:10000) 7f4405d00000(10000:10000) 7f4405ce0000(10000:10000) 7f4405cd0000(10000:10000) 7f4405cb0000(10000:f000) 7f4405c90000(10000:f000) 7f4405bd0000(10000:10000) 7f4405b40000(10000:10000) 7f4405b20000(10000:f000) 7f4405af0000(10000:10000) 7f4405ad0000(10000:10000) 7f4405aa0000(30000:2f000) 7f4405a90000(10000:e000) 7f4405a70000(10000:10000) 7f4405a30000(10000:10000) 7f4405a10000(10000:10000) 7f44059a0000(10000:10000) 7f4405980000(10000:10000) 7f4405970000(10000:10000) 7f4405950000(10000:10000) 7f4405730000(10000:10000) 7f4405720000(10000:10000) 7f4405700000(10000:10000) 7f44056e0000(10000:10000) 7f44056b0000(10000:10000) 7f44056a0000(10000:10000) 7f4405670000(10000:10000) 7f4405650000(10000:10000) 7f4405630000(10000:f000) 7f44055e0000(10000:10000) 7f44055d0000(10000:10000) 7f44055c0000(10000:10000) 7f44055a0000(10000:10000) 7f4405590000(10000:10000) 7f4405580000(10000:10000) 7f4405560000(10000:10000) 7f4405540000(10000:10000) 7f4405520000(10000:10000) 7f4405510000(10000:10000) 7f44054f0000(10000:10000) 7f44054e0000(10000:10000) 7f44054c0000(10000:10000) 7f44054a0000(10000:10000) 7f4405490000(10000:10000) 7f4405280000(10000:d000) 7f4405260000(10000:10000) 7f4405240000(10000:10000) 7f4405230000(10000:10000) 7f4405210000(10000:10000) 7f4405200000(10000:10000) 7f44051f0000(10000:10000) 7f44051a0000(10000:10000) 7f4405180000(10000:10000) 7f4405150000(10000:10000) 7f4405120000(10000:10000) 7f44050e0000(10000:10000) 7f4405050000(10000:10000) 7f4405020000(10000:10000) 7f4404ff0000(10000:10000) 7f4404fc0000(10000:10000) 7f4404fb0000(10000:10000) 7f4404f90000(10000:f000) 7f4404f50000(10000:10000) 7f4404f40000(10000:10000) 7f4404f30000(10000:10000) 7f4404f10000(10000:7000) 7f4404e60000(10000:10000) 7f4404e50000(10000:e000) 7f4404e30000(10000:d000) 7f4404d20000(10000:10000) 7f4404d10000(10000:10000) 7f4404cf0000(10000:7000) 7f4404c70000(10000:10000) 7f4404c50000(10000:10000) 7f4404a40000(10000:b000) 7f4404a20000(10000:e000) 7f4404a00000(10000:10000) 7f4404970000(10000:10000) 7f4404950000(10000:10000) 7f4404940000(10000:10000) 7f4404930000(10000:7000) 7f4404920000(10000:f000) 7f4404900000(10000:10000) 7f44048e0000(10000:10000) 7f4404830000(10000:10000) 7f4404810000(10000:10000) 7f44047f0000(10000:c000) 7f4404710000(10000:10000) 7f44046f0000(10000:10000) 7f44046d0000(10000:10000) 7f44046a0000(10000:10000) 7f4404550000(10000:10000) 7f44044a0000(10000:10000) 7f44043d0000(10000:10000) 7f44043c0000(10000:10000) 7f44040c0000(10000:b000) 7f4404000000(10000:e000) 7f4403ed0000(10000:10000) 7f4403e30000(10000:10000) 7f4403d10000(10000:10000) 7f4403d00000(10000:f000) 7f4403c90000(10000:a000) 7f4403bc0000(10000:10000) 7f4403bb0000(10000:f000) 7f44037a0000(10000:a000) 7f4403770000(10000:10000) 7f4403730000(10000:10000) 7f44036d0000(10000:10000) 7f4403660000(10000:10000) 7f4403510000(10000:10000) 7f44033f0000(10000:c000) 7f44032a0000(10000:10000) 7f4403280000(10000:10000) 7f4403270000(10000:f000) 7f4403220000(40000:3f000) 7f4402980000(20000:1a000) 7f4402650000(10000:d000) 7f4402620000(10000:d000) 7f4402580000(10000:10000) 7f4402360000(10000:f000) 7f4402210000(10000:10000) 7f4402140000(10000:10000) 7f4402050000(10000:10000) 7f4402030000(10000:10000) 7f4401ee0000(10000:10000) 7f4401d70000(10000:10000) 7f4401cb0000(10000:10000) 7f4401ad0000(10000:10000) 7f44018b0000(10000:f000) 7f44016d0000(10000:10000) 7f4401520000(10000:10000) 7f44013b0000(10000:10000) 7f4401210000(20000:1f000) 7f4400f90000(10000:3000) 7f4400de0000(10000:10000) 7f4400d20000(10000:10000) 7f4400b00000(10000:f000) 7f4400a20000(10000:10000) 7f4400870000(10000:10000) 7f4400730000(20000:20000) 7f44004d0000(10000:2000) 7f4400280000(10000:10000) 7f4400200000(10000:e000) 7f44001f0000(10000:9000) 7f4400100000(10000:10000) 7f44000f0000(10000:10000) 7f44000a0000(10000:10000) 7f4400000000(10000:10000) 7f43fffb0000(50000:50000) 7f43ff4e0000(3000:1000) Size: 0xd3b000 (13873152) bytes total, 0xb4000 (737280) bytes wasted.
HighFrequencyHeap:    7f4406cd0000(10000:f000) 7f4406cb0000(10000:10000) 7f4406c70000(10000:10000) 7f4406c60000(10000:10000) 7f4406c40000(10000:10000) 7f4406c30000(10000:10000) 7f4406c10000(10000:10000) 7f4406bf0000(10000:10000) 7f4406bd0000(10000:10000) 7f4406b70000(10000:10000) 7f4406b50000(10000:10000) 7f4406b40000(10000:10000) 7f4406b10000(10000:10000) 7f4406af0000(10000:10000) 7f4406ae0000(10000:10000) 7f4406ac0000(10000:10000) 7f4406ab0000(10000:10000) 7f4406a80000(10000:10000) 7f4406a60000(10000:10000) 7f4406a50000(10000:10000) 7f4406a40000(10000:10000) 7f4406a30000(10000:10000) 7f4406a10000(10000:10000) 7f44069f0000(10000:10000) 7f4406980000(10000:10000) 7f4406730000(10000:10000) 7f4406710000(10000:10000) 7f44066f0000(10000:10000) 7f44066c0000(10000:10000) 7f44066a0000(10000:10000) 7f4406680000(10000:10000) 7f4406620000(10000:10000) 7f4406550000(10000:10000) 7f44063a0000(10000:10000) 7f4406380000(10000:f000) 7f4406320000(10000:10000) 7f4406310000(10000:10000) 7f44062f0000(10000:f000) 7f44062d0000(10000:10000) 7f4406280000(10000:10000) 7f4406230000(10000:f000) 7f44061c0000(10000:10000) 7f44061a0000(10000:10000) 7f4406180000(10000:10000) 7f4406150000(10000:10000) 7f4406130000(10000:10000) 7f4406100000(10000:10000) 7f44060f0000(10000:10000) 7f4405ec0000(10000:10000) 7f4405ea0000(10000:10000) 7f4405e80000(10000:10000) 7f4405e60000(10000:10000) 7f4405e30000(10000:10000) 7f4405d60000(10000:10000) 7f4405d40000(10000:10000) 7f4405d10000(10000:10000) 7f4405cf0000(10000:10000) 7f4405cc0000(10000:10000) 7f4405ca0000(10000:10000) 7f4405c80000(10000:10000) 7f4405b50000(10000:10000) 7f4405b30000(10000:10000) 7f4405b10000(10000:10000) 7f4405b00000(10000:10000) 7f4405ae0000(10000:10000) 7f4405a80000(10000:f000) 7f4405a60000(10000:10000) 7f4405a20000(10000:10000) 7f4405990000(10000:10000) 7f4405960000(10000:10000) 7f4405740000(10000:10000) 7f4405710000(10000:10000) 7f44056f0000(10000:10000) 7f44056d0000(10000:10000) 7f44056c0000(10000:10000) 7f4405680000(10000:10000) 7f4405660000(10000:10000) 7f4405640000(10000:10000) 7f4405610000(10000:10000) 7f4405600000(10000:10000) 7f44055f0000(10000:10000) 7f44055b0000(10000:10000) 7f4405570000(10000:10000) 7f4405530000(10000:10000) 7f4405500000(10000:10000) 7f44054d0000(10000:10000) 7f44054b0000(10000:10000) 7f4405270000(10000:10000) 7f4405250000(10000:10000) 7f4405220000(10000:10000) 7f44051e0000(10000:10000) 7f4405190000(10000:10000) 7f4405170000(10000:10000) 7f4405160000(10000:10000) 7f4405140000(10000:10000) 7f4405130000(10000:10000) 7f4405110000(10000:10000) 7f4405100000(10000:10000) 7f44050f0000(10000:10000) 7f4405060000(10000:10000) 7f4405040000(10000:10000) 7f4405030000(10000:10000) 7f4405010000(10000:10000) 7f4405000000(10000:10000) 7f4404fe0000(10000:10000) 7f4404fd0000(10000:10000) 7f4404fa0000(10000:10000) 7f4404f80000(10000:10000) 7f4404f60000(10000:10000) 7f4404f20000(10000:10000) 7f4404f00000(10000:10000) 7f4404e40000(10000:10000) 7f4404d00000(10000:10000) 7f4404cd0000(10000:10000) 7f4404c60000(10000:10000) 7f4404a30000(10000:10000) 7f4404a10000(10000:10000) 7f4404980000(10000:10000) 7f4404960000(10000:10000) 7f4404910000(10000:10000) 7f44048f0000(10000:10000) 7f44048d0000(10000:10000) 7f44048c0000(10000:10000) 7f4404820000(10000:10000) 7f4404800000(10000:10000) 7f44047e0000(10000:10000) 7f4404700000(10000:10000) 7f44046e0000(10000:10000) 7f44046c0000(10000:10000) 7f44046b0000(10000:10000) 7f4404690000(10000:10000) 7f44045b0000(10000:10000) 7f4404540000(10000:10000) 7f44044b0000(10000:10000) 7f4404490000(10000:10000) 7f4404480000(10000:10000) 7f44043e0000(10000:10000) 7f4404020000(10000:10000) 7f4404010000(10000:10000) 7f4403fa0000(10000:10000) 7f4403ee0000(10000:10000) 7f4403e40000(10000:10000) 7f4403d30000(10000:10000) 7f4403d20000(10000:10000) 7f4403ca0000(10000:10000) 7f4403c70000(10000:10000) 7f4403c60000(10000:10000) 7f4403bd0000(10000:10000) 7f44037b0000(10000:10000) 7f4403790000(10000:10000) 7f4403780000(10000:10000) 7f4403760000(10000:10000) 7f4403750000(10000:10000) 7f4403740000(10000:10000) 7f4403720000(10000:10000) 7f4403710000(10000:10000) 7f44036e0000(10000:10000) 7f44036c0000(10000:10000) 7f44036b0000(10000:10000) 7f4403420000(10000:10000) 7f4403410000(10000:10000) 7f4403400000(10000:10000) 7f44033a0000(10000:10000) 7f4403330000(10000:10000) 7f4403290000(10000:10000) 7f4403260000(10000:10000) 7f4402660000(10000:10000) 7f4402640000(10000:10000) 7f4402630000(10000:10000) 7f4402590000(10000:10000) 7f4402480000(10000:10000) 7f4402370000(10000:10000) 7f44022e0000(10000:10000) 7f4402230000(10000:10000) 7f4402220000(10000:10000) 7f4402130000(10000:10000) 7f4402120000(10000:10000) 7f4402070000(10000:10000) 7f4402060000(10000:10000) 7f4402040000(10000:10000) 7f4402020000(10000:10000) 7f4401df0000(10000:10000) 7f4401d80000(10000:10000) 7f4401d60000(10000:10000) 7f4401d00000(10000:10000) 7f44019c0000(10000:10000) 7f4401850000(10000:10000) 7f44016e0000(10000:10000) 7f4401600000(10000:10000) 7f4401510000(10000:10000) 7f44012e0000(10000:10000) 7f4400f20000(10000:10000) 7f4400d40000(10000:10000) 7f4400d30000(10000:10000) 7f4400b70000(10000:10000) 7f4400a30000(10000:10000) 7f44009c0000(10000:10000) 7f44004e0000(10000:10000) 7f4400110000(10000:10000) 7f44000e0000(10000:10000) 7f44000d0000(10000:10000) 7f44000c0000(10000:10000) 7f44000b0000(10000:10000) 7f4400010000(10000:10000) 7f43ff4e4000(9000:5000) Size: 0xcc0000 (13369344) bytes total, 0x8000 (32768) bytes wasted.
StubHeap:             7f4405070000(10000:5000) 7f43ff4ed000(3000:3000) Size: 0x8000 (32768) bytes total.
IndirectionCellHeap:  7f4405690000(10000:5000) 7f43ff4f0000(6000:6000) Size: 0xb000 (45056) bytes total.
LookupHeap:           7f4404f70000(10000:6000) 7f43ff4ff000(4000:4000) Size: 0xa000 (40960) bytes total.
ResolveHeap:          7f43ff534000(57000:20000) Size: 0x20000 (131072) bytes total.
DispatchHeap:         7f43ff503000(31000:e000) Size: 0xe000 (57344) bytes total.
CacheEntryHeap:       7f4406220000(10000:3000) 7f43ff4f6000(9000:9000) Size: 0xc000 (49152) bytes total.
Total size:           Size: 0x1a52000 (27598848) bytes total, 0xbc000 (770048) bytes wasted.
----------------------------------------
Domain 1:             5558fbe901d0
LoaderAllocator:      5558fbe901d0
No unique loader heaps found.
----------------------------------------
JIT Manager:          5558fbe936c0
LoaderCodeHeap:       7f4400020000(80000:36000) Size: 0x36000 (221184) bytes total.
LoaderCodeHeap:       7f4401940000(80000:39000) Size: 0x39000 (233472) bytes total.
LoaderCodeHeap:       7f44032b0000(80000:42000) Size: 0x42000 (270336) bytes total.
LoaderCodeHeap:       7f4403be0000(80000:49000) Size: 0x49000 (299008) bytes total.
LoaderCodeHeap:       7f44044c0000(80000:5a000) Size: 0x5a000 (368640) bytes total.
LoaderCodeHeap:       7f4404840000(80000:5f000) Size: 0x5f000 (389120) bytes total.
LoaderCodeHeap:       7f4404a50000(200000:18f000) Size: 0x18f000 (1634304) bytes total.
LoaderCodeHeap:       7f4405290000(200000:1c4000) Size: 0x1c4000 (1851392) bytes total.
LoaderCodeHeap:       7f4405750000(200000:1b1000) Size: 0x1b1000 (1773568) bytes total.
LoaderCodeHeap:       7f4405ee0000(200000:1b6000) Size: 0x1b6000 (1794048) bytes total.
LoaderCodeHeap:       7f4406740000(200000:15f000) Size: 0x15f000 (1437696) bytes total.
HostCodeHeap:         7f4403c80000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4404ce0000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4405a50000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4405d80000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4406120000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4406260000(10000:10000) Size: 0x10000 (65536) bytes total.
HostCodeHeap:         7f4406290000(40000:40000) Size: 0x40000 (262144) bytes total.
HostCodeHeap:         7f4406340000(40000:40000) Size: 0x40000 (262144) bytes total.
HostCodeHeap:         7f4406b80000(40000:40000) Size: 0x40000 (262144) bytes total.
Total size:           Size: 0xaec000 (11452416) bytes total.
----------------------------------------

========================================
Number of GC Heaps: 1
----------------------------------------
generation 0 starts at 7f423d1f52c8
generation 1 starts at 7f423cc8b198
generation 2 starts at 7f43c7fff000
ephemeral segment allocation context: none
Small object heap
         segment            begin        allocated        committed allocated size         committed size        
    7f419d080000     7f419d081000     7f41acef9e78     7f41acf1a000 0xfe78e78 (266833528)  0xfe9a000 (266969088) 
    7f41bbfe0000     7f41bbfe1000     7f41cbb9b950     7f41cbbbc000 0xfbba950 (263956816)  0xfbdc000 (264093696) 
    7f4203ff0000     7f4203ff1000     7f4213feffb0     7f4213ff0000 0xfffefb0 (268431280)  0x10000000 (268435456)
    7f422bffa000     7f422bffb000     7f423a3a02f0     7f423a3c1000 0xe3a52f0 (238703344)  0xe3c7000 (238841856) 
    7f428bff0000     7f428bff1000     7f429bfeffe0     7f429bff0000 0xfffefe0 (268431328)  0x10000000 (268435456)
    7f429bff2000     7f429bff3000     7f42abff2000     7f42abff2000 0xffff000 (268431360)  0x10000000 (268435456)
    7f42abffe000     7f42abfff000     7f42bbffdfd0     7f42bbffe000 0xfffefd0 (268431312)  0x10000000 (268435456)
    7f42d3ffe000     7f42d3fff000     7f42e3ffe000     7f42e3ffe000 0xffff000 (268431360)  0x10000000 (268435456)
    7f42fb030000     7f42fb031000     7f430b030000     7f430b030000 0xffff000 (268431360)  0x10000000 (268435456)
    7f4317ffe000     7f4317fff000     7f4327ffe000     7f4327ffe000 0xffff000 (268431360)  0x10000000 (268435456)
    7f43c7ffe000     7f43c7fff000     7f43d7ffdfe8     7f43d7ffe000 0xfffefe8 (268431336)  0x10000000 (268435456)
    7f423bffe000     7f423bfff000     7f423d33f780     7f423e51b000 0x1340780 (20187008)   0x251d000 (38916096)  
Large object heap starts at 7f43d7fff000
         segment            begin        allocated        committed allocated size         committed size        
    7f40ed054000     7f40ed055000     7f40f1055038     7f40f1056000 0x4000038 (67108920)   0x4002000 (67117056)  
    7f41d3fe4000     7f41d3fe5000     7f41d7fe5038     7f41d8006000 0x4000038 (67108920)   0x4022000 (67248128)  
    7f41dbfe6000     7f41dbfe7000     7f41dffe7038     7f41e0008000 0x4000038 (67108920)   0x4022000 (67248128)  
    7f41ebfea000     7f41ebfeb000     7f41effeb038     7f41f000c000 0x4000038 (67108920)   0x4022000 (67248128)  
    7f41f3fec000     7f41f3fed000     7f41f7fed038     7f41f800e000 0x4000038 (67108920)   0x4022000 (67248128)  
    7f41fbfee000     7f41fbfef000     7f41fffef038     7f4200010000 0x4000038 (67108920)   0x4022000 (67248128)  
    7f432fffe000     7f432ffff000     7f4337063918     7f4337084000 0x7064918 (117852440)  0x7086000 (117989376) 
    7f43d7ffe000     7f43d7fff000     7f43de5eb030     7f43de5ec000 0x65ec030 (106872880)  0x65ee000 (106881024) 
Pinned object heap starts at 7f43dffff000
         segment            begin        allocated        committed allocated size         committed size        
    7f43dfffe000     7f43dffff000     7f43e02bc4f8     7f43e02c0000 0x2bd4f8 (2872568)     0x2c2000 (2891776)    
------------------------------
GC Allocated Heap Size:    Size: 0xd4a1f110 (3567382800) bytes.
GC Committed Heap Size:    Size: 0xd5d3c000 (3587424256) bytes.

Total bytes consumed by CLR: 0xd827a000 (3626475520)

可以看到,CLR 吃掉了3.6G。Ok,到这儿基本可以确定是托管内存问题。

相信大家都注意到小对象堆和大对象堆有大量的内存段,吃掉了主要的托管内存。

接下来使用
dumpheap -stat
来统计下托管堆上内存所有对象:

0:000> !sos dumpheap -stat
Statistics:
          MT      Count   TotalSize Class Name
7f4406bf5ef0          1          24 NPOI.OpenXmlFormats.ExtendedPropertiesDocument
7f4406bf60a0          1          24 NPOI.OpenXmlFormats.CustomPropertiesDocument
7f4406bf67e8          1          24 NPOI.OpenXmlFormats.CT_CustomProperties
7f4406bf74e0          1          24 NPOI.OpenXmlFormats.CT_Property[]
7f4406bf8540          1          24 NPOI.HSSF.Util.HSSFColor+Black
7f4406bf8678          1          24 NPOI.HSSF.Util.HSSFColor+White
...
...
7f440213aa08      3,270     313,920 System.Reflection.Emit.DynamicMethod
7f44000c8080      4,135     358,828 System.Int32[]
7f4401519498     10,309     365,192 System.RuntimeType[]
7f440480b990          2     458,800 StackExchange.Redis.RawResult[]
7f440213faa0      3,270     470,880 System.Reflection.Emit.DynamicILGenerator
7f440185dcf8      5,133     492,768 System.Reflection.RuntimeParameterInfo
7f44000e7e60      3,216     551,160 System.String[]
7f440185e0a8      6,986     558,880 System.Signature
7f44012e8f10     10,812   1,124,448 System.Reflection.RuntimeMethodInfo
7f44000c7bc8     15,112   1,305,648 System.SByte[]
7f44000cd2e0     30,854   2,294,568 System.String
7f4400110eb0        224   5,064,502 System.Char[]
7f4400b784f0     18,748  21,734,523 System.Byte[]
7f44046efe70  6,541,452 418,652,928 System.EventHandler<StackExchange.Redis.EndPointEventArgs>
7f44047001b0  6,541,452 418,652,928 System.EventHandler<StackExchange.Redis.HashSlotMovedEventArgs>
7f4404700488  6,541,452 418,652,928 System.EventHandler<StackExchange.Redis.InternalErrorEventArgs>
7f44046efc10  6,541,452 418,652,928 System.EventHandler<StackExchange.Redis.RedisErrorEventArgs>
5558fbe656a0    293,562 474,504,032 Free
7f440001b0f8      5,222 538,033,592 System.Object[]
7f44046ef988 13,082,904 837,305,856 System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs>
Total 39,789,084 objects, 3,567,106,882 bytes

这个命令会按照
mt
汇总统计每个对象的数量及所占内存大小,并按内存大小排序,我们看到询问有几个对象格外抢眼,其中最后一个有 1000w 个
EvevtHandler
对象,占 837 M,那就继续往下看。

罪魁祸首

我们需要看一下这 1000w 对象到底什么来头,因为上述是按
mt
汇总的,所以现在用
dumpheap -mt
展开看一下:

0:000> !sos dumpheap -mt 7f44046ef988
         Address               MT           Size
    7f419d0810c0     7f44046ef988             64 
    7f419d081100     7f44046ef988             64 
    7f419d081240     7f44046ef988             64  
...
    7f419d2f0e50     7f44046ef988             64 
    7f419d2f0f90     7f44046ef988             64 
    7f419d2f0fd0     7f44046ef988             64 

Statistics:
          MT  Count TotalSize Class Name
7f44046ef988 13,160   842,240 System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs>
Total 13,160 objects, 842,240 bytes

对象太多,不可能展示完,所以这里做了一个省略。好的,接下来抽查几个对象使用命令
dumpobj
看一下:

0:000> !sos dumpobj /d 7f419d2f0fd0
Name:        System.EventHandler`1[[StackExchange.Redis.ConnectionFailedEventArgs, StackExchange.Redis]]
MethodTable: 00007f44046ef988
EEClass:     00007f44046f8b58
Tracked Type: false
Size:        64(0x40) bytes
File:        /usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.6/System.Private.CoreLib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007f4400015290  40001bd        8        System.Object  0 instance 00007f419d2f0fd0 _target
00007f4400015290  40001be       10        System.Object  0 instance 0000000000000000 _methodBase
00007f44000c3eb8  40001bf       18        System.IntPtr  1 instance 00007F43FF4ED1E0 _methodPtr
00007f44000c3eb8  40001c0       20        System.IntPtr  1 instance 00007F4403BEF508 _methodPtrAux
00007f4400015290  4000243       28        System.Object  0 instance 0000000000000000 _invocationList
00007f44000c3eb8  4000244       30        System.IntPtr  1 instance 0000000000000000 _invocationCount

抽查几个后发现,都是一样。那既然这些对象一直不被GC回收,肯定有什么原因,使用
gcroot
查看一下都被谁引用:

0:000> !sos gcroot 7f419d2f0fd0
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.

HandleTable:
    00007f447a911310 (strong handle)
          -> 7f43e0008c28     System.Object[] 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer (static variable: System.Object._locker)
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

Thread 1f4f:
    7f44724de9b0 7f440488ac93 StackExchange.Redis.PhysicalBridge+<ProcessBacklogAsync>d__97.MoveNext()
        rbp-78: 00007f44724de9c8
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

        rbp-80: 00007f44724de9c0
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

    7f44724dea50 7f44048888f9
        rbp-8: 00007f44724dea78
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

    7f44724dea90 7f44048882fb
        rbp-10: 00007f44724dea90
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

    7f44724deab0 7f44048882ac StackExchange.Redis.PhysicalBridge.ProcessBacklogAsync()
        rbp-30: 00007f44724deab0
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

        rbp-8: 00007f44724dead8
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

    7f44724deaf0 7f4404887f3f StackExchange.Redis.PhysicalBridge+<>c.<StartBacklogProcessor>b__93_0(System.Object)
        rbp-10: 00007f44724deaf0
          -> 7f43c81b7e30     StackExchange.Redis.PhysicalBridge 
          -> 7f43c81a4aa0     StackExchange.Redis.ConnectionMultiplexer 
          -> 7f423d33c828     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 
          -> 7f41f3fed020     System.Object[] 
          -> 7f419d2f0fd0     System.EventHandler<StackExchange.Redis.ConnectionFailedEventArgs> 

Found 8 unique roots.

注意:因为对象太多,遍历查找引用链需要一点儿时间,请耐心等待!

我们发现,它们都被
StackExchange.Redis.ConnectionMultiplexer
引用,这是一个 redis 连接对象。再经过大胆猜测和简单分析,其余几个巨量对象列表都是这个 redis 对象的事件。说明程序中引入了 StackExchange 的 redis 扩展包,并在项目中某处使用了 redis 操作,这个对象到底在何处,可能出问题的代码又在哪儿?

定位代码

有经验的朋友可能会知道,redis 这个对象大概率是一个静态变量。现在我们来验证一下,试图查找这个对象对应的静态变量所在位置,使用
s-q
命令查找是哪个内存地址(B)上保存此这个 redis 对象地址(A)。

首先缩小内存查找范围:

0:000> !sos dumpobj /d 7f43e0008c28
Name:        System.Object[]
MethodTable: 00007f440001b0f8
EEClass:     00007f440001b070
Tracked Type: false
Size:        32664(0x7f98) bytes
Array:       Rank 1, Number of elements 4080, Type CLASS (Print Array)
Fields:
None

我们需要用到这个 Object 对象数组的起始位置及数量用作如下命令的偏移:

0:000> s-q 7f43e0008c28 L?0x7f98 7f43c81a4aa0
00007f43`e000aaa0  00007f43`c81a4aa0 00007f43`ca4922a0

这样,我们就找到了静态变量的位置(B):

00007f43`e000aaa0

让我们继续查找是哪些代码在使用这个静态变量:

0:000> s-b 0 L?0xffffffffffffffff a0 aa 00 e0 43 7f 00 00
00007f44`044f9f81  a0 aa 00 e0 43 7f 00 00-33 ff 48 89 38 48 b8 6c  ....C...3.H.8H.l
00007f44`044fb96a  a0 aa 00 e0 43 7f 00 00-48 83 3e 00 40 0f 94 c6  ....C...H.>.@...
00007f44`044fb9b3  a0 aa 00 e0 43 7f 00 00-48 83 3f 00 74 38 48 bf  ....C...H.?.t8H.
00007f44`044fb9c3  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 b8 28 b0 6e  ....C...H.?H.(.n
00007f44`044fba71  a0 aa 00 e0 43 7f 00 00-48 8b b5 50 ff ff ff e8  ....C...H..P....
00007f44`044fbaa9  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 7d b8 48  ....C...H.?H.}.H
00007f44`044fbb03  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 7d a8 48  ....C...H.?H.}.H
00007f44`044fbb5d  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 7d 98 48  ....C...H.?H.}.H
00007f44`044fbbb7  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 7d 88 48  ....C...H.?H.}.H
00007f44`044fbc11  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 bd 78 ff  ....C...H.?H..x.
00007f44`044fbc74  a0 aa 00 e0 43 7f 00 00-48 8b 3f 48 89 bd 68 ff  ....C...H.?H..h.
00007f44`044fbcc7  a0 aa 00 e0 43 7f 00 00-48 8b 00 48 89 45 d0 90  ....C...H..H.E..

OK 我们找到不少,抽几个看一下反汇编代码:

0:000> !sos u 00007f44`044fb96a
Normal JIT generated code
Wisder.xRiver.BaseCore.Util.xRedis.get_Instance()
ilAddr is 00007F447142F990 pImport is 00000143049FEE00
Begin 00007F44044FB890, size 485
00007f44`044fb890 55              push    rbp
00007f44`044fb891 4881ecd0000000  sub     rsp,0D0h
00007f44`044fb898 488dac24d0000000 lea     rbp,[rsp+0D0h]
00007f44`044fb8a0 33c0            xor     eax,eax
00007f44`044fb8a2 48898538ffffff  mov     qword ptr [rbp-0C8h],rax
00007f44`044fb8a9 c4413857c0      vxorps  xmm8,xmm8,xmm8
00007f44`044fb8ae 48b840ffffffffffffff mov rax,0FFFFFFFFFFFFFF40h
00007f44`044fb8b8 c5797f0428      vmovdqa xmmword ptr [rax+rbp],xmm8
00007f44`044fb8bd c5797f440510    vmovdqa xmmword ptr [rbp+rax+10h],xmm8
00007f44`044fb8c3 c5797f440520    vmovdqa xmmword ptr [rbp+rax+20h],xmm8
00007f44`044fb8c9 4883c030        add     rax,30h
00007f44`044fb8cd 75e9            jne     00007f44`044fb8b8
00007f44`044fb8cf 4889a530ffffff  mov     qword ptr [rbp-0D0h],rsp
00007f44`044fb8d6 48b870e26402447f0000 mov rax,7F440264E270h
00007f44`044fb8e0 833800          cmp     dword ptr [rax],0
00007f44`044fb8e3 7405            je      00007f44`044fb8ea
00007f44`044fb8e5 e88659dd74      call    libcoreclr!JIT_DbgIsJustMyCode (00007f44`792d1270)
00007f44`044fb8ea 90              nop
00007f44`044fb8eb 48bf90aa00e0437f0000 mov rdi,7F43E000AA90h
00007f44`044fb8f5 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fb8f8 393f            cmp     dword ptr [rdi],edi
00007f44`044fb8fa e829ccb2fb      call    00007f44`00028528 (System.String.get_Length(), mdToken: 0000000006000764)
00007f44`044fb8ff 8945cc          mov     dword ptr [rbp-34h],eax
00007f44`044fb902 837dcc00        cmp     dword ptr [rbp-34h],0
00007f44`044fb906 400f94c6        sete    sil
00007f44`044fb90a 400fb6f6        movzx   esi,sil
00007f44`044fb90e 8975fc          mov     dword ptr [rbp-4],esi
00007f44`044fb911 837dfc00        cmp     dword ptr [rbp-4],0
00007f44`044fb915 7451            je      00007f44`044fb968
00007f44`044fb917 90              nop
00007f44`044fb918 48bf90ec0c00447f0000 mov rdi,7F44000CEC90h (MT: System.Exception)
00007f44`044fb922 e809c8dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fb927 48898540ffffff  mov     qword ptr [rbp-0C0h],rax
00007f44`044fb92e bf49220000      mov     edi,2249h
00007f44`044fb933 48be00de6402447f0000 mov rsi,7F440264DE00h
00007f44`044fb93d e87ecedc74      call    libcoreclr!JIT_StrCns (00007f44`792c87c0)
00007f44`044fb942 48898538ffffff  mov     qword ptr [rbp-0C8h],rax
00007f44`044fb949 488bb538ffffff  mov     rsi,qword ptr [rbp-0C8h]
00007f44`044fb950 488bbd40ffffff  mov     rdi,qword ptr [rbp-0C0h]
00007f44`044fb957 e89cd1b2fb      call    00007f44`00028af8 (System.Exception..ctor(System.String), mdToken: 00000000060003CB)
00007f44`044fb95c 488bbd40ffffff  mov     rdi,qword ptr [rbp-0C0h]
00007f44`044fb963 e8f832dd74      call    libcoreclr!IL_Throw (00007f44`792cec60)
00007f44`044fb968 48bea0aa00e0437f0000 mov rsi,7F43E000AAA0h
00007f44`044fb972 48833e00        cmp     qword ptr [rsi],0
00007f44`044fb976 400f94c6        sete    sil
00007f44`044fb97a 400fb6f6        movzx   esi,sil
00007f44`044fb97e 8975f8          mov     dword ptr [rbp-8],esi
00007f44`044fb981 837df800        cmp     dword ptr [rbp-8],0
00007f44`044fb985 0f8409010000    je      00007f44`044fba94
00007f44`044fb98b 90              nop
00007f44`044fb98c 48be98aa00e0437f0000 mov rsi,7F43E000AA98h
00007f44`044fb996 488b36          mov     rsi,qword ptr [rsi]
00007f44`044fb999 488975f0        mov     qword ptr [rbp-10h],rsi
00007f44`044fb99d 33f6            xor     esi,esi
00007f44`044fb99f 8975e8          mov     dword ptr [rbp-18h],esi
00007f44`044fb9a2 488d75e8        lea     rsi,[rbp-18h]
00007f44`044fb9a6 488b7df0        mov     rdi,qword ptr [rbp-10h]
00007f44`044fb9aa e811dbb2fb      call    00007f44`000294c0 (System.Threading.Monitor.Enter(System.Object, Boolean ByRef), mdToken: 0000000006002BA0)
00007f44`044fb9af 90              nop
00007f44`044fb9b0 90              nop
00007f44`044fb9b1 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fb9bb 48833f00        cmp     qword ptr [rdi],0
00007f44`044fb9bf 7438            je      00007f44`044fb9f9
00007f44`044fb9c1 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fb9cb 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fb9ce 48b828b06e04447f0000 mov rax,7F44046EB028h
00007f44`044fb9d8 393f            cmp     dword ptr [rdi],edi
00007f44`044fb9da ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.get_IsConnected(), mdToken: 00000000060001A2)
00007f44`044fb9dc 89854cffffff    mov     dword ptr [rbp-0B4h],eax
00007f44`044fb9e2 83bd4cffffff00  cmp     dword ptr [rbp-0B4h],0
00007f44`044fb9e9 400f94c7        sete    dil
00007f44`044fb9ed 400fb6ff        movzx   edi,dil
00007f44`044fb9f1 89bd64ffffff    mov     dword ptr [rbp-9Ch],edi
00007f44`044fb9f7 eb0a            jmp     00007f44`044fba03
00007f44`044fb9f9 c78564ffffff01000000 mov dword ptr [rbp-9Ch],1
00007f44`044fba03 8bbd64ffffff    mov     edi,dword ptr [rbp-9Ch]
00007f44`044fba09 400fb6ff        movzx   edi,dil
00007f44`044fba0d 897de4          mov     dword ptr [rbp-1Ch],edi
00007f44`044fba10 837de400        cmp     dword ptr [rbp-1Ch],0
00007f44`044fba14 7470            je      00007f44`044fba86
00007f44`044fba16 90              nop
00007f44`044fba17 48bf90aa00e0437f0000 mov rdi,7F43E000AA90h
00007f44`044fba21 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fba24 e86ff6ffff      call    00007f44`044fb098 (StackExchange.Redis.ConfigurationOptions.Parse(System.String), mdToken: 0000000006000156)
00007f44`044fba29 48898558ffffff  mov     qword ptr [rbp-0A8h],rax
00007f44`044fba30 488bbd58ffffff  mov     rdi,qword ptr [rbp-0A8h]
00007f44`044fba37 48897dd8        mov     qword ptr [rbp-28h],rdi
00007f44`044fba3b 488b7dd8        mov     rdi,qword ptr [rbp-28h]
00007f44`044fba3f be10270000      mov     esi,2710h
00007f44`044fba44 393f            cmp     dword ptr [rdi],edi
00007f44`044fba46 e8edf7ffff      call    00007f44`044fb238 (StackExchange.Redis.ConfigurationOptions.set_ConnectTimeout(Int32), mdToken: 0000000006000121)
00007f44`044fba4b 90              nop
00007f44`044fba4c 488b7dd8        mov     rdi,qword ptr [rbp-28h]
00007f44`044fba50 be983a0000      mov     esi,3A98h
00007f44`044fba55 393f            cmp     dword ptr [rdi],edi
00007f44`044fba57 e82cf9ffff      call    00007f44`044fb388 (StackExchange.Redis.ConfigurationOptions.set_SyncTimeout(Int32), mdToken: 000000000600014B)
00007f44`044fba5c 90              nop
00007f44`044fba5d 488b7dd8        mov     rdi,qword ptr [rbp-28h]
00007f44`044fba61 33f6            xor     esi,esi
00007f44`044fba63 e838e3ffff      call    00007f44`044f9da0 (StackExchange.Redis.ConnectionMultiplexer.Connect(StackExchange.Redis.ConfigurationOptions, System.IO.TextWriter), mdToken: 00000000060001BC)
00007f44`044fba68 48898550ffffff  mov     qword ptr [rbp-0B0h],rax
00007f44`044fba6f 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fba79 488bb550ffffff  mov     rsi,qword ptr [rbp-0B0h]
00007f44`044fba80 e80cb7f274      call    libcoreclr!JIT_CheckedWriteBarrier (00007f44`79427191)
00007f44`044fba85 90              nop
00007f44`044fba86 90              nop
00007f44`044fba87 90              nop
00007f44`044fba88 eb00            jmp     00007f44`044fba8a
00007f44`044fba8a 488bfc          mov     rdi,rsp
00007f44`044fba8d e854020000      call    00007f44`044fbce6 (Wisder.xRiver.BaseCore.Util.xRedis.get_Instance(), mdToken: 00000000060000B3)
00007f44`044fba92 90              nop
00007f44`044fba93 90              nop
00007f44`044fba94 48bf88f96e04447f0000 mov rdi,7F44046EF988h (MT: System.EventHandler`1[[StackExchange.Redis.ConnectionFailedEventArgs, StackExchange.Redis]])
00007f44`044fba9e e88dc6dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbaa3 488945c0        mov     qword ptr [rbp-40h],rax
00007f44`044fbaa7 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbab1 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbab4 48897db8        mov     qword ptr [rbp-48h],rdi
00007f44`044fbab8 488b7dc0        mov     rdi,qword ptr [rbp-40h]
00007f44`044fbabc 33f6            xor     esi,esi
00007f44`044fbabe 48ba10f5be03447f0000 mov rdx,7F4403BEF510h
00007f44`044fbac8 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbad2 e8b1b6b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbad7 488b7db8        mov     rdi,qword ptr [rbp-48h]
00007f44`044fbadb 488b75c0        mov     rsi,qword ptr [rbp-40h]
00007f44`044fbadf 48b818b16e04447f0000 mov rax,7F44046EB118h
00007f44`044fbae9 393f            cmp     dword ptr [rdi],edi
00007f44`044fbaeb ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_ConnectionFailed(System.EventHandler`1<StackExchange.Redis.ConnectionFailedEventArgs>), mdToken: 00000000060001F9)
00007f44`044fbaed 90              nop
00007f44`044fbaee 48bf88f96e04447f0000 mov rdi,7F44046EF988h (MT: System.EventHandler`1[[StackExchange.Redis.ConnectionFailedEventArgs, StackExchange.Redis]])
00007f44`044fbaf8 e833c6dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbafd 488945b0        mov     qword ptr [rbp-50h],rax
00007f44`044fbb01 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbb0b 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbb0e 48897da8        mov     qword ptr [rbp-58h],rdi
00007f44`044fbb12 488b7db0        mov     rdi,qword ptr [rbp-50h]
00007f44`044fbb16 33f6            xor     esi,esi
00007f44`044fbb18 48ba08f5be03447f0000 mov rdx,7F4403BEF508h
00007f44`044fbb22 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbb2c e857b6b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbb31 488b7da8        mov     rdi,qword ptr [rbp-58h]
00007f44`044fbb35 488b75b0        mov     rsi,qword ptr [rbp-50h]
00007f44`044fbb39 48b838b16e04447f0000 mov rax,7F44046EB138h
00007f44`044fbb43 393f            cmp     dword ptr [rdi],edi
00007f44`044fbb45 ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_ConnectionRestored(System.EventHandler`1<StackExchange.Redis.ConnectionFailedEventArgs>), mdToken: 00000000060001FF)
00007f44`044fbb47 90              nop
00007f44`044fbb48 48bf10fc6e04447f0000 mov rdi,7F44046EFC10h (MT: System.EventHandler`1[[StackExchange.Redis.RedisErrorEventArgs, StackExchange.Redis]])
00007f44`044fbb52 e8d9c5dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbb57 488945a0        mov     qword ptr [rbp-60h],rax
00007f44`044fbb5b 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbb65 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbb68 48897d98        mov     qword ptr [rbp-68h],rdi
00007f44`044fbb6c 488b7da0        mov     rdi,qword ptr [rbp-60h]
00007f44`044fbb70 33f6            xor     esi,esi
00007f44`044fbb72 48ba00f5be03447f0000 mov rdx,7F4403BEF500h
00007f44`044fbb7c 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbb86 e8fdb5b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbb8b 488b7d98        mov     rdi,qword ptr [rbp-68h]
00007f44`044fbb8f 488b75a0        mov     rsi,qword ptr [rbp-60h]
00007f44`044fbb93 48b878b16e04447f0000 mov rax,7F44046EB178h
00007f44`044fbb9d 393f            cmp     dword ptr [rdi],edi
00007f44`044fbb9f ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_ErrorMessage(System.EventHandler`1<StackExchange.Redis.RedisErrorEventArgs>), mdToken: 000000000600020F)
00007f44`044fbba1 90              nop
00007f44`044fbba2 48bf70fe6e04447f0000 mov rdi,7F44046EFE70h (MT: System.EventHandler`1[[StackExchange.Redis.EndPointEventArgs, StackExchange.Redis]])
00007f44`044fbbac e87fc5dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbbb1 48894590        mov     qword ptr [rbp-70h],rax
00007f44`044fbbb5 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbbbf 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbbc2 48897d88        mov     qword ptr [rbp-78h],rdi
00007f44`044fbbc6 488b7d90        mov     rdi,qword ptr [rbp-70h]
00007f44`044fbbca 33f6            xor     esi,esi
00007f44`044fbbcc 48baf8f4be03447f0000 mov rdx,7F4403BEF4F8h
00007f44`044fbbd6 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbbe0 e8a3b5b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbbe5 488b7d88        mov     rdi,qword ptr [rbp-78h]
00007f44`044fbbe9 488b7590        mov     rsi,qword ptr [rbp-70h]
00007f44`044fbbed 48b848b16e04447f0000 mov rax,7F44046EB148h
00007f44`044fbbf7 393f            cmp     dword ptr [rdi],edi
00007f44`044fbbf9 ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_ConfigurationChanged(System.EventHandler`1<StackExchange.Redis.EndPointEventArgs>), mdToken: 0000000006000202)
00007f44`044fbbfb 90              nop
00007f44`044fbbfc 48bfb0017004447f0000 mov rdi,7F44047001B0h (MT: System.EventHandler`1[[StackExchange.Redis.HashSlotMovedEventArgs, StackExchange.Redis]])
00007f44`044fbc06 e825c5dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbc0b 48894580        mov     qword ptr [rbp-80h],rax
00007f44`044fbc0f 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbc19 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbc1c 4889bd78ffffff  mov     qword ptr [rbp-88h],rdi
00007f44`044fbc23 488b7d80        mov     rdi,qword ptr [rbp-80h]
00007f44`044fbc27 33f6            xor     esi,esi
00007f44`044fbc29 48ba18f5be03447f0000 mov rdx,7F4403BEF518h
00007f44`044fbc33 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbc3d e846b5b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbc42 488bbd78ffffff  mov     rdi,qword ptr [rbp-88h]
00007f44`044fbc49 488b7580        mov     rsi,qword ptr [rbp-80h]
00007f44`044fbc4d 48b868b16e04447f0000 mov rax,7F44046EB168h
00007f44`044fbc57 393f            cmp     dword ptr [rdi],edi
00007f44`044fbc59 ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_HashSlotMoved(System.EventHandler`1<StackExchange.Redis.HashSlotMovedEventArgs>), mdToken: 000000000600020C)
00007f44`044fbc5b 90              nop
00007f44`044fbc5c 48bf88047004447f0000 mov rdi,7F4404700488h (MT: System.EventHandler`1[[StackExchange.Redis.InternalErrorEventArgs, StackExchange.Redis]])
00007f44`044fbc66 e8c5c4dc74      call    libcoreclr!JIT_NewS_MP_FastPortable (00007f44`792c8130)
00007f44`044fbc6b 48898570ffffff  mov     qword ptr [rbp-90h],rax
00007f44`044fbc72 48bfa0aa00e0437f0000 mov rdi,7F43E000AAA0h
00007f44`044fbc7c 488b3f          mov     rdi,qword ptr [rdi]
00007f44`044fbc7f 4889bd68ffffff  mov     qword ptr [rbp-98h],rdi
00007f44`044fbc86 488bbd70ffffff  mov     rdi,qword ptr [rbp-90h]
00007f44`044fbc8d 33f6            xor     esi,esi
00007f44`044fbc8f 48ba20f5be03447f0000 mov rdx,7F4403BEF520h
00007f44`044fbc99 48b9e0d14eff437f0000 mov rcx,7F43FF4ED1E0h
00007f44`044fbca3 e8e0b4b2fb      call    00007f44`00027188 (System.MulticastDelegate.CtorOpened(System.Object, IntPtr, IntPtr), mdToken: 00000000060004ED)
00007f44`044fbca8 488bbd68ffffff  mov     rdi,qword ptr [rbp-98h]
00007f44`044fbcaf 488bb570ffffff  mov     rsi,qword ptr [rbp-90h]
00007f44`044fbcb6 48b828b16e04447f0000 mov rax,7F44046EB128h
00007f44`044fbcc0 393f            cmp     dword ptr [rdi],edi
00007f44`044fbcc2 ff10            call    qword ptr [rax] (StackExchange.Redis.ConnectionMultiplexer.add_InternalError(System.EventHandler`1<StackExchange.Redis.InternalErrorEventArgs>), mdToken: 00000000060001FC)
00007f44`044fbcc4 90              nop
00007f44`044fbcc5 48b8a0aa00e0437f0000 mov rax,7F43E000AAA0h
00007f44`044fbccf 488b00          mov     rax,qword ptr [rax]
00007f44`044fbcd2 488945d0        mov     qword ptr [rbp-30h],rax
00007f44`044fbcd6 90              nop
00007f44`044fbcd7 eb00            jmp     00007f44`044fbcd9
00007f44`044fbcd9 488b45d0        mov     rax,qword ptr [rbp-30h]
00007f44`044fbcdd 4881c4d0000000  add     rsp,0D0h
00007f44`044fbce4 5d              pop     rbp
00007f44`044fbce5 c3              ret
00007f44`044fbce6 55              push    rbp
00007f44`044fbce7 4883ec10        sub     rsp,10h
00007f44`044fbceb 488b2f          mov     rbp,qword ptr [rdi]
00007f44`044fbcee 48892c24        mov     qword ptr [rsp],rbp
00007f44`044fbcf2 488dadd0000000  lea     rbp,[rbp+0D0h]
00007f44`044fbcf9 8b7de8          mov     edi,dword ptr [rbp-18h]
00007f44`044fbcfc 400fb6ff        movzx   edi,dil
00007f44`044fbd00 85ff            test    edi,edi
00007f44`044fbd02 740a            je      00007f44`044fbd0e
00007f44`044fbd04 488b7df0        mov     rdi,qword ptr [rbp-10h]
00007f44`044fbd08 e83317dd74      call    libcoreclr!JIT_MonExit_Portable (00007f44`792cd440)
00007f44`044fbd0d 90              nop
00007f44`044fbd0e 90              nop
00007f44`044fbd0f 4883c410        add     rsp,10h
00007f44`044fbd13 5d              pop     rbp
00007f44`044fbd14 c3              ret

重点来了,
Wisder.xRiver.BaseCore.Util.xRedis.get_Instance()
这个方法其实是
Instance
属性的
get
方法。

可以继续使用
ip2md
命令将指令地址转换出模块信息:

0:000> !sos ip2md 00007f44`044fb96a
MethodDesc:   00007f4403caf880
Method Name:          Wisder.xRiver.BaseCore.Util.xRedis.get_Instance()
Class:                00007f4403d1a278
MethodTable:          00007f4403cafe48
mdToken:              00000000060000B3
Module:               00007f440264de00
IsJitted:             yes
Current CodeAddr:     00007f44044fb890
Version History:
  ILCodeVersion:      0000000000000000
  ReJIT ID:           0
  IL Addr:            00007f447142f990
     CodeAddr:           00007f44044fb890  (MinOptJitted)
     NativeCodeVersion:  0000000000000000

找到模块后,查看一下信息就可以找到对应的 dll 文件:

0:000> !sos DumpModule /d 00007f440264de00
Name: /xxx/xRiver.BaseCore.dll
Attributes:              PEFile IsFileLayout 
TransientFlags:          00209011 
Assembly:                00005558fc05f540
BaseAddress:             00007F447142A000
PEAssembly:              00005558FC03ABE0
ModuleId:                00007F440264E990
ModuleIndex:             0000000000000064
LoaderHeap:              0000000000000000
TypeDefToMethodTableMap: 00007F4402650000
TypeRefToMethodTableMap: 00007F4402650560
MethodDefToDescMap:      00007F4402650EE8
FieldDefToDescMap:       00007F4402653550
MemberRefToDescMap:      0000000000000000
FileReferencesMap:       00007F4402655948
AssemblyReferencesMap:   00007F4402655950
MetaData start address:  00007F4471444540 (148124 bytes)

那么接下来就是找到这个 dll 文件对应的源码项目,重点关注一下里面
get_Instance
方法。

namespace Wisder.xRiver.BaseCore.Util
{
    public static class xRedis
    {
        private static ConnectionMultiplexer _instance = null;

        /// <summary>
        /// 使用一个静态属性来返回已连接的实例,如下列中所示。
        /// 这样,一旦 ConnectionMultiplexer 断开连接,便可以初始化新的连接实例。
        /// </summary>
        public static ConnectionMultiplexer Instance
        {
            get
            {
                if (Constr.Length == 0)
                {
                    throw new Exception("Redis连接字符串未设置!");
                }

                if (_instance == null)
                {
                    lock (_locker)
                    {
                        if (_instance == null || !_instance.IsConnected)
                        {
                            var options = ConfigurationOptions.Parse(Constr);
                            options.ConnectTimeout = 10000; // 设置连接超时时间为10秒
                            options.SyncTimeout = 15000; // 设置同步操作的超时时间为15秒

                            _instance = ConnectionMultiplexer.Connect(options); //Constr
                        }
                    }
                }

                //注册如下事件
                _instance.ConnectionFailed += MuxerConnectionFailed;
                _instance.ConnectionRestored += MuxerConnectionRestored;
                _instance.ErrorMessage += MuxerErrorMessage;
                _instance.ConfigurationChanged += MuxerConfigurationChanged;
                _instance.HashSlotMoved += MuxerHashSlotMoved;
                _instance.InternalError += MuxerInternalError;
                return _instance;
            }
        }
    }
}

解决思路:把事件注册挪到最里面的
if
内部,
_instance
赋值的后面。后续修改后可以观察下情况。

总结分析

一个事件注册会产生一个 64 Byte 大小的对象,不要小看它,积少成多,照样会导致内存泄露问题。

这让我不由得想起了 PHP 语言,一个请求结束就会销毁进程,可有效避免内存泄露问题,实在不行再重启一下 php 服务,也可以解决内存泄露问题。回到 .net 这一类平台上,就可以好好写代码,提高代码质量,避免内存泄露等问题。