研发协同平台数据库死锁处理及改进

源宝导读:数据库死锁是高并发复杂系统都要面临课题,处理死锁问题没有一招制敌的标准方法,需要具体问题具体分析。本文将基于研发协同平台遇到的死锁案例,介绍从监控、分析到处理的完整过程和经验总结。

一、背景

      研发协同平台使用的技术栈大体是.NET Core + EFCore + SQLServer, 周边还有一些第三方组件, 如Redis、Jenkins、Gitlab、Sonar。整体技术架构又分为前台服务(rdc_service)、调度服务(rdc_service)、更新服务(rdc_upgrade)。其中更新服务面向所有终端用户的服务器,用于客户产品更新;调度服务作为后台作业,定时对客户的服务器进行巡检,发现异常服务器时通知客户;客户和客户服务器的数量数以万计,接口请求量较大,更新服务和调度服务都是对客户及其服务器数据操作,两者之间难免会有数据上的交际,这样一来,死锁就有了可乘之机,本文重点介绍研发协同平台对死锁的监控和解决方案。

二、死锁监控

    在早期SQL Server中,通过跟踪标志1204/1222,可以在数据库错误日志中捕获的死锁信息。跟踪标志1204会报告由死锁所涉及的每个节点设置格式的死锁信息。跟踪标志 1222 会设置死锁信息的格式,顺序为先按进程,然后按资源。可以同时启用这两个跟踪标志,以获取同一个死锁事件的两种表示形式。

    下面的示例显示启用跟踪标志 1204 时的输出。在此示例中,节点 1 中的表为没有索引的堆,节点 2 中的表为具有非聚集索引的堆。节点 2 中索引键在发生死锁时正在进行更新。

    在工作负载密集型系统上使用跟踪标志1204和1222可能会导致性能,为了解决这一问题,自 SQL Server 2012 (11.x) 起,SQL Server提供了xml_deadlock_report 扩展事件 (xEvent),来帮助研发人员更加便捷高效的监测及排查死锁。下图展示了在扩展事件中可以捕获的deadlock相关事件:

    在扩展事件启用后,当数据库发生死锁时,SQL Server会自动将导致死锁的相关会话数据,以XML的形式保存下来,下图展示了研发协同平台近期监控到的死锁记录:

三、死锁分析

    随机查看几个死锁日志,所有的记录均属于同一场景:进程1持有表CustomerServer行A的排他(X)锁,需要申请表Customer行B的共享(S)锁,而进程2刚好持有Customer行B的排他(X)锁,且正在向CustomerServer表行A申请更新(U)锁。众所周知,排他(X)锁和更新(U)锁完全互斥,故两个进程陷入了相互等待的僵局。下图1展示了死锁发生时两个进程的相关数据,图2则显示了双方在资源上面的争用情况。

    除非某个外部进程断开死锁,否则死锁中的两个事务都将无限期等待下去。SQL Server 数据库引擎死锁监视器定期检查陷入死锁的任务。如果监视器检测到循环依赖关系,将选择其中一个任务作为牺牲品,然后终止其事务并提示错误。其他任务就可以完成其事务。

    另外需要注意的是:死锁经常与正常阻塞混淆。事务请求的资源被其他事务锁定时,发出请求的事务一直等到该锁被释放。除非设置了 LOCK_TIMEOUT,否则 SQL Server 事务不会超时。拥有锁的事务完成并释放锁后,发出请求的事务将获取锁并继续执行,所以该事务是被阻塞,而不是陷入了死锁。

    默认情况下,SQL Server 数据库引擎选择运行回滚开销最小的事务的会话作为死锁牺牲品。此外,用户也可以使用 SET DEADLOCK_PRIORITY 语句指定死锁情况下会话的优先级。

3.1、死锁业务场景

    进一步分析死锁报告,可以直观的看到涉及死锁的两个进程来至3台服务器,这里我们假定是服务器A、B、C。3台服务器分别属于进程1调度服务(服务器A)、进程2更新服务(服务器B、C)。调度服务是一组后台作业任务的集合,其中有同时访问表Customer\CustomerServer的任务只有一个,即客户服务器状态同步作业:扫描系统中所有客户的服务器,只要客户服务器未按照约定上报信息,则认为其离线,将离线状态同步至客户表(Customer)和客户服务器表(CustomerServer),并给客户对应的负责人发送邮件。该作业每分钟执行一次,每次作业任务耗时50s,执行时间过长可能是导致频繁死锁的原因之一。

    通过死锁报告中的服务器信息可以快速的定位到进程1的业务场景,那么进程2则需要其他工具来帮助我们定位。进程2更新服务是一组API集合,供用户更新客户环境的相关业务,其中涉及Customer\CustomerServer两张表的操作非常多,如果心跳接口、注册客户信息接口。到这一步,我们所知道的信息如下:

    死锁报告的中的SQL脚本是由EFCore生成的,表面上看不出来进程2到底是哪一个接口。这里我们借助SkyWalking 的端点埋点数据,下图1是所有心跳接口的错误记录,时间点与死锁日志中的时间完全匹配,没有例外;图2是错误详情,其中的事务进程Id和死锁牺牲对象与死锁中报告完全匹配。

3.2、主外键对执行计划的影响

    SQL Server 数据库引擎提供了访问查询执行计划的运行时信息。出现性能问题时,最重要的操作之一是准确了解正在执行的工作负载以及如何驱动使用资源。为此,访问实际执行计划将很重要。这里的更新服务心跳接口,只有对客户服务器表(CustomerServer)访问,并没有直接对Customer表进行访问,通过SkyWalking的端点数据,以及死锁报告中的SQL脚本,可以拿到心跳接口的估算执行计划。尽管业务场景只需要更新一个Status字段,但由于未开启EFCore的模型跟踪(ChangeTracker),EFCore生成的SQL脚本显示的更新了除主键以外所有字段。其中[CustomerId]和[PluginId]正好是外键字段,分别来自Customer和Plugin表,为了保证主外键的约束,一条简单的单表update操作,将涉及到3张表的资源访问。下图展示了心跳接口在SQL Server中的执行计划:

    如果开启的模型跟踪,或者手动写编写SQL,按需更新字段,上面的死锁将不会存在,下图展示了单表单字段场景下的执行计划:

四、业务改进

    我们通过简单的SQL改造,在不影响业务的情况下,将进程2的SQL资源访问由3张表降低至单表操作,这里不敢保证说已经消除了死锁,但至少将死锁降低到一个可以忽略不计的范围。为了保险起见,这里继续对进程1(调度服务Job)优化。

    上述提到客户服务器状态同步作业每分钟执行一次,每次作业任务耗时50s,这里的耗时有点过长,Job在全天24小时内,有83%(50s/60s)的时间处于作业中,为了保证原子性,这里采用是的方法级事务,事务锁定的范围是整个作业周期,即时50s,并且不论客户的服务器状态是否发生改变,这里都会修改数据,下面是Job相关的代码:

    在不改变业务场景的基础上,我们对作业Job的执行策略做了微调:

  • 修改正式环境时间间隔为10分钟

  • 更新方式调整为更新指定字段

  • 每个客户单独开启事务处理

  • 状态未发生变化时不修改数据

    客户环境的服务器比较复杂,通常来说偶尔的抖动属于正常现象,心跳的有效期都是3-5分钟,每分钟去检测一次状态有点多余,这里将作业时间由每分钟调整10分钟,可以有效降低后台作业与前端API接口之间的资源冲突;更新方式采用EFCore的模型跟踪(ChangeTracker),按需更新,这里既然只检测状态变更,就只需要更改状态,减少修改的字段,可以避免由主外键引起的额外共享锁申请;第3条和第4条都是为了降低事务的范围,这里的事务原子性控制到单个客户明显比全局性价比高,没必要因为一个客户的失败,导致整个作业失败,当客户服务器状态没有发生变化时,对当前客户的处理应直接跳过,减少修改次数;通过上面4条优化策略,任务的作业时间由之前的50s降低为15s,且不会对业务产生影响,死锁也随时消除。

五、写在最后

    尽管死锁不能完全避免,但遵守特定的编码习惯可以将发生死锁的机会降至最低。在这里分享几点研发协同平台团队的开发规范,有助于大家在日常开发中避免死锁和提高数据库性能:

  • 缩小事务的范围,提前计算出需要更新的数据,避免在事务中做额外的业务逻辑计算;

  • 按需更新行字段,EFCore类似的ORM框架,带来了开发上的便捷,同时也会带来性能上的隐患;

  • 外键字段加索引,外键字段索引可以显著降低查询或更新时,降低对数据资源锁定的范围;

  • 定期优化慢SQL ,慢SQL意味着需研发协同平台持续交付2.0架构演进要读取更多索引页或者数据页,加大资源锁定的范围;

作者简介

冯同学: 研发工程师,目前负责研发协同平台的设计与开发工作。

也许您还想看

研发协同平台持续集成Jenkins作业设计演进

研发协同平台持续交付之代理服务实践

研发协同平台持续集成2.0架构演进

研发协同平台持续交付2.0架构演进

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/309158.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java substring() 方法

substring() 方法返回字符串的子字符串。 语法 public String substring(int beginIndex)或public String substring(int beginIndex, int endIndex)参数 beginIndex – 起始索引(包括), 索引从 0 开始。 endIndex – 结束索引(不包括&…

简单说说async/await

小明用async/await写了几年的异步方法,但总没有完全理解里面的机制,他决定去请教邻居小花。小花听了小明的描述后说:首先你要明白异步的根本是什么?大白话解释异步就是:拉一个人(线程)帮着做一些…

Newtonsoft 六个超简单又实用的特性,值得一试 【下篇】

一:讲故事上一篇介绍的 6 个特性从园子里的反馈来看效果不错,那这一篇就再带来 6 个特性同大家一起欣赏。二:特性分析1. 像弱类型语言一样解析 json大家都知道弱类型的语言有很多,如: nodejs,python,php&am…

C++实现双栈结构(一个顺序表中使用两个栈)

因为平常栈中push的数据不会太多&#xff0c;为了节约空间&#xff0c;所以可以在一个顺序表中使用两个栈 结构图: 在这里我会留一个空间用来判断栈是否满&#xff01; #include <iostream> using namespace std; typedef int ElemType;class DoubleStack { private:El…

Redis凭啥这么快?只能做缓存?架构师道出了真相(颠覆你的认知)

Redis到底有多快Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库&#xff0c;由C语言编写&#xff0c;官方提供的数据是可以达到100000的QPS&#xff08;每秒内查询次数&#xff09;。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差&…

Java面向对象编程(基础部分)

面向对象编程(基础部分) 类与对象 01&#xff1a; public class ObjectWorkDemo {public static void main(String[] args){Cat cat1 new Cat();cat1.name "Tom";cat1.age 3;cat1.color "white";Cat cat2 new Cat();cat2.name "xiaohua"…

Blazor带我重玩前端(一)

写在前面曾经我和前端朋友聊天的时候&#xff0c;我说我希望有一天可以用C#写前端&#xff0c;不过当时更多的是美好的想象&#xff0c;这一切正变得真实……什么是Blazor我们知道浏览器可以正确解释并执行JavaScript代码&#xff0c;那么浏览器是如何执行C#代码的呢&#xff1…

Java面向对象编程(中级)

面向对象编程(中级) 包 访问修饰符 封装 01: public class Encapsulation01 {public static void main(String[] args){Person person new Person();person.name "Tom";person.setAge(30);person.setSalary(30000);} }class Person {public String name;private…

[Mvp.Blazor] 动态路由与钩子函数

&#xff08;Blazor组件的生命周期函数&#xff09;一直在学习也没有停下脚步&#xff0c;用着脑子还是挺好的&#xff0c;感觉可以更脚踏实地一下。最近偶尔也继续看了看Blazor&#xff0c;毕竟我也开源了一个项目嘛&#xff0c;基本我正式开源的项目都会负责到底&#xff0c;…

Java面向对象编程(高级)

面向对象编程(高级) 类变量和类方法 01: package ChildDemo;public class Child {private String name;public static int cnt 0;public Child(String name){this.name name;}public void join(){System.out.println(name "join the game");} }//package ChildDe…

.NET Core + Kubernetes:Volume

和 Docker 类似&#xff0c;Kubernetes 中也提供了 Volume 来实现数据卷挂载&#xff0c;但 Kubernetes 中 Volume 是基于 Pod&#xff0c;而不是容器&#xff0c;它可被 Pod 中多个容器共享&#xff0c;另外 Kubernetes 中提供比较丰富的 Volume 类型[1]&#xff0c;如&#…

WPF中的Data Binding调试指南

点击蓝字“大白技术控”关注我哟加个“星标★”&#xff0c;每日良时&#xff0c;好文必达&#xff01;WPF中的Data Binding如何Debug?大家平时做WPF开发&#xff0c;相信用Visual studio的小伙伴比较多。XAML代码曾经在某些特殊版本的Visual Studio中是可以加断点进行调试的&…

.NET 5 尝鲜 - 开源项目TerminalMACS WPF管理端支持.NET 5

点击上方“Dotnet9”添加关注哦聊天界面设计TerminalMACS一个使用 Prism 作为模块化框架、基于多个开源控件库作为UI控件选择、集成开源 UI 界面设计的 .NET 5 WPF 客户端项目。项目名称&#xff1a;TerminalMACS WPF管理端项目开源地址&#xff1a;Github&#xff1a;https://…

FreeSql.Generator命令行代码生成器是如何实现的

目录FreeSql介绍FreeSql.GeneratorRazorEngine.NetCore源码解析FreeSql.ToolsFreeSqlFreeSql 是功能强大的对象关系映射技术(O/RM)&#xff0c;支持 .NETCore 2.1 或 .NETFramework 4.0 或 Xamarin。有一个强大的ORM&#xff0c;也方便我们开发一个代码生成器。一般情况下&…

Java IDEA断点调试

断点调试(debug) 断点调试应用案例 01&#xff1a; package Assign;public class Debug01 {public static void main(String[] args) {int sum 0;for (int i 0;i<5;i){sumi;System.out.println(i);System.out.println(sum);}System.out.println("continue");} …

.NET Core请求控制器Action方法正确匹配,但为何404?

【导读】提前预祝各位端午节快乐。有时候我们会发现方法名称都正确匹配&#xff0c;但就是找不到对应请求接口&#xff0c;所以本文我们来深入了解下何时会出现接口请求404的情况。匹配控制器Action方法&#xff08;404&#xff09;首先我们创建一个web api应用程序&#xff0c…

布斯乘法以及带符号数的运算

乘法理解 对于最熟悉的十进制乘法很最基本的运算原理就是一个乘数乘以另一个乘数的个位、十位、百位数字然后求和。比如 放到二进制来看其实它也是这样的&#xff0c;多位数的乘法就是一个乘数乘上另一个乘数的各位求和。那么&#xff1a; 布斯算法及原理 原理 已经知道两…

angular 接入 IdentityServer4

angular 接入 IdentityServer4Intro最近把活动室预约的项目做了一个升级&#xff0c;预约活动室需要登录才能预约&#xff0c;并用 IdentityServer4 做了一个统一的登录注册中心&#xff0c;这样以后就可以把其他的需要用户操作的应用统一到 IdentityServer 这里&#xff0c;这…

主机Redis服务迁移到现有Docker Overlay网络

“《麻雀虽小&#xff0c;五脏俱全》之主机现有Redis服务迁移到Docker Swarm Overlay网络&#xff0c;并搭建高可用容器集群。hello, 好久不见&#xff0c;之前文章记录了一个实战的2C分布式项目的改造过程&#xff0c;结果如下&#xff1a;其中Redis并未完成容器化改造&#x…

Java控制结构

控制结构 程序流程控制介绍 顺序控制 分支控制if-else 单分支 案例演示 01: import java.util.Scanner; public class IfWorkDemo {public static void main(String[] args){Scanner myScanner new Scanner(System.in);System.out.println("input your age");int…