[C#]通用守护进程服务_weixin_34121304的博客-CSDN博客

mikel阅读(517)

来源: [C#]通用守护进程服务_weixin_34121304的博客-CSDN博客

摘要

很多情况下,都会使用windows服务做一些任务,但总会有一些异常,导致服务停止。这个时候,开发人员又不能立马解决问题,所以做一个守护者服务还是很有必要的。当检测到服务停止了,重启一下服务,等开发人员到位了,再排查错误日志。

代码

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <appSettings>
    <!--要守护的服务的服务名称-->
    <add key="toWatchServiceName" value=""/>
    <!--守护服务的名称-->
    <add key="serviceName" value="邮件提醒服务守护服务"/>
    <!--每1分钟检查一次 以秒为单位-->
    <add key="timerInterval" value="60"/>
  </appSettings>
</configuration>

服务

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace WindowsService.Watch
{
    partial class ServiceWather : ServiceBase
    {
        private static string currentExePath = string.Empty;
        public ServiceWather()
        {
            InitializeComponent();
            currentExePath = AppDomain.CurrentDomain.BaseDirectory;
        }
        /// <summary>
        /// 检查间隔
        /// </summary>
        private static readonly int _timerInterval = Convert.ToInt32(ConfigurationManager.AppSettings["timerInterval"]) * 1000;
        /// <summary>
        /// 要守护的服务名
        /// </summary>
        private static readonly string toWatchServiceName = ConfigurationManager.AppSettings["toWatchServiceName"];
        private System.Timers.Timer _timer;
        protected override void OnStart(string[] args)
        {
            //服务启动时开启定时器
            _timer = new System.Timers.Timer();
            _timer.Interval = _timerInterval;
            _timer.Enabled = true;
            _timer.AutoReset = true;
            _timer.Elapsed += _timer_Elapsed;
            LogHelper.WriteLog(currentExePath, "守护服务开启");
        }

        void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            //如果服务状态为停止,则重新启动服务
            if (!CheckSericeStart(toWatchServiceName))
            {
                StartService(toWatchServiceName);
            }
        }

        protected override void OnStop()
        {
            if (_timer != null)
            {
                _timer.Stop();
                _timer.Dispose();
                LogHelper.WriteLog(currentExePath, "守护服务停止");
            }
        }
        /// <summary>
        /// 启动服务
        /// </summary>
        /// <param name="serviceName">要启动的服务名称</param>
        private void StartService(string serviceName)
        {
            try
            {
                ServiceController[] services = ServiceController.GetServices();
                foreach (ServiceController service in services)
                {
                    if (service.ServiceName.Trim() == serviceName.Trim())
                    {
                        service.Start();
                        //直到服务启动
                        service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, 30));
                        LogHelper.WriteLog(currentExePath, string.Format("启动服务:{0}", serviceName));
                    }
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog(currentExePath, ex);
            }
        }
        private bool CheckSericeStart(string serviceName)
        {
            bool result = true;
            try
            {
                ServiceController[] services = ServiceController.GetServices();
                foreach (ServiceController service in services)
                {
                    if (service.ServiceName.Trim() == serviceName.Trim())
                    {
                        if ((service.Status == ServiceControllerStatus.Stopped)
                            || (service.Status == ServiceControllerStatus.StopPending))
                        {
                            result = false;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog(currentExePath, ex);
            }
            return result;
        }
        /// <summary>
        /// 停止
        /// </summary>
        /// <param name="serviceName"></param>
        private void StopService(string serviceName)
        {
            try
            {
                ServiceController[] services = ServiceController.GetServices();
                foreach (ServiceController service in services)
                {
                    if (service.ServiceName.Trim() == serviceName.Trim())
                    {
                        service.Stop();
                        //直到服务停止
                        service.WaitForStatus(ServiceControllerStatus.Stopped, new TimeSpan(0, 0, 30));
                        LogHelper.WriteLog(currentExePath, string.Format("启动服务:{0}", serviceName));
                    }
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog(currentExePath, ex);
            }
        }
    }
}

C#写进程守护程序 - 逐梦先生 - 博客园

mikel阅读(1099)

来源: C#写进程守护程序 – 逐梦先生 – 博客园

最近写了好多次进程守护程序,今天在这里总结一下。
用到的知识点:

1、在程序中启动进程,

2、写Windows服务,

3、以及在Windows服务中启动带界面的程序

关于第三点的问题,我在我的上一篇博客单独介绍了解决方案:C#做服务使用Process启动外部程序没有界面

这里主要讲述怎样用C#做一个服务这个服务可以监视某个进程,如果进程不存在则启动这个进程(还可以通过让监视程序和进程守护服务定期交换数据的方法来更准确的进行监测)

新建Windows服务项目

实现服务要执行的操作

查看services代码,在对应位置写入要执行的代码:

protected override void OnStart(string[] args)
       {
           //服务开启执行代码
       }
       protected override void OnStop()
       {
           //服务结束执行代码
       }
       protected override void OnPause()
       {
           //服务暂停执行代码
           base.OnPause();
       }
       protected override void OnContinue()
       {
           //服务恢复执行代码
           base.OnContinue();
       }
       protected override void OnShutdown()
       {
           //系统即将关闭执行代码
           base.OnShutdown();
       }

 

添加安装程序

1、在services的设计界面右键,选择添加安装程序:

生成serviceInstaller1和 serviceProcessInstaller1两个组件 。

2、把serviceInstaller1的属性ServiceName改写为你的服务程序名,并把启动模式设置为AUTOMATIC

3、把serviceProcessInstaller1的属性account改写为 LocalSystem

4、最后通过从生成菜单中选择生成来生成项目

安装卸载Windows服务

如果你在你需要的函数里面写过你需要的方法后,直接点运行是不可运行的。

1、安装Windows服务

安装命令:InstallUtil.exe MyServiceLog.exe

InstallUtil存在路径为:C:\WINDOWS\Microsoft.NET\Framework\.NET版本号

复制C:\WINDOWS\Microsoft.Net\Framework\版本号  路径中的InstallUtil.exe 到bin/Debug或bin/release文件夹中

在命令行窗口中(管理员权限打开),cd到服务Debug目录下运行安装命令,安装过程如下图所示:

安装成功之后手动启动服务:

2、卸载window 服务

命令:InstallUtil.exe MyServiceLog.exe /u

如果修改这个服务,但是路径没有变化的话是不需要重新注册服务的,直接停止服务,然后用新的文件覆盖原来的文件即可,如果路径发生变化,应该先卸载这个服务,然后重新安装这个服务。

SqlServer批量删除表_超人不会飞-CSDN博客

mikel阅读(820)

select ‘drop table ‘+name+’;’ from sys.tables where name like ‘DataSyncV1DelaySample%’ or name like ‘DataSyncV2DelaySample%’

来源: SqlServer批量删除表_超人不会飞-CSDN博客

最近需要删除一批曾经用来存放日志的表,这些表数量很多而且占用了大量的磁盘空间,不得不删除,释放相应的磁盘空间。但是一张一张的手动来删除比较麻烦,在网上找了小技巧,只需要三步,就可以实现批量删除。

第一步
执行SQL语句,我的表名都是以’DataSyncV1DelaySample或者’DataSyncV2DelaySample开头的,执行下面的语句得到一批drop table的脚本,后面的where条件可以根据自身的需求进行修改。

select ‘drop table ‘+name+’;’ from sys.tables where name like ‘DataSyncV1DelaySample%’ or name like ‘DataSyncV2DelaySample%’
1

第二步
复制脚本,执行

第三步
删除了表并不意味着,磁盘空间被释放了,还需要做一些操作,右键相应的数据库->任务->收缩->数据库,点击确定。期间可能需要点时间,执行完毕后,数据库占用的磁盘空间就被释放了。
————————————————
版权声明:本文为CSDN博主「我是李超人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012062455/article/details/79312086

常用的几个测试网络连接的命令_曲健磊的技术分享-CSDN博客_网络测试常用命令

mikel阅读(1256)

来源: 常用的几个测试网络连接的命令_曲健磊的技术分享-CSDN博客_网络测试常用命令

记录几个常用的测试网络连接的dos命令。

ping
tracert
netstat
ipconfig/all
1.ping命令是用来测试网络之间是否能够连通以及网络之间的传输速度。

例:测试网卡,TCP/IP协议是否可用,如果发送的数据包数目等于接收的数据包数目(也就是本机能够接收到目的地址发送的数据包),就证明网卡正常,TCP/IP协议可用。

127.0.0.1是本地回环地址,也就是本机地址( 自己电脑的地址 )。

ping命令实际上是通过向目的地址发送数据包,然后检测目的地址是否回发数据包来判断网络之间的连通性,默认发送4个数据包,每个数据包32字节。也可以通过设置参数来调整数据包的个数,以及每个数据包的大小(字节数)。

ping命令的几个常用参数:
-n 定义发送数据包的个数,默认4个

-l 设定数据包的大小,默认32字节,最大65500字节

-t 不断的ping目标主机(就相当于死循环),知道按Ctrl + C手动停止

-a 显示目的地址的主机名

2.tracert命令用来跟踪从本机到目的地址所经过的路由。
例:跟踪从本机到京东所要经过的路由

3.netstat命令用来查看最近活动的网络(也就是你最近访问了那些网站

4.ipconfig命令是用来查看本机的ip地址(v4 or v6),加上/all就是获取更详细的信息。

————————————————
版权声明:本文为CSDN博主「曲健磊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a909301740/article/details/78509037

c#守护进程(windows服务监测程序,程序关闭后自启动)最详细!!!!!!!!_Struggle_Cxg的博客-CSDN博客_c#服务监控进程

mikel阅读(2482)

来源: c#守护进程(windows服务监测程序,程序关闭后自启动)最详细!!!!!!!!_Struggle_Cxg的博客-CSDN博客_c#服务监控进程

最近项目需要:程序关闭后自动重新启动,需要一个监测程序所以写下这篇文章,为自己以后留个印象,也给大家一个参考,不喜勿喷!!!

1.打开VS创建windows服务

2.实现服务的操作步骤(查看service1代码)

3.(右键)添加引用(这个dll是为显示界面的,很多人说windows服务开启了,程序也在运行就是不显示界面那就需要这个了)
下载地址: 链接:https://pan.baidu.com/s/1R2AFVxLArZCkwCumlUxN1w 密码:4qhn

记得添加命名空间!!!
引用dll需要用到的代码
try
{
//appStartPath = “程序路径”;
IntPtr userTokenHandle = IntPtr.Zero;
ApiDefinitions.WTSQueryUserToken(ApiDefinitions.WTSGetActiveConsoleSessionId(), ref userTokenHandle);

ApiDefinitions.PROCESS_INFORMATION procInfo = new ApiDefinitions.PROCESS_INFORMATION();
ApiDefinitions.STARTUPINFO startInfo = new ApiDefinitions.STARTUPINFO();
startInfo.cb = (uint)System.Runtime.InteropServices.Marshal.SizeOf(startInfo);

ApiDefinitions.CreateProcessAsUser(
userTokenHandle,
appStartPath,
“”,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
null,
ref startInfo,
out procInfo);

if (userTokenHandle != IntPtr.Zero)
ApiDefinitions.CloseHandle(userTokenHandle);

int _currentAquariusProcessId = (int)procInfo.dwProcessId;
}
catch (Exception ex)
{

}
4.在对应位置写入代码
protected override void OnStart(string[] args)
{
//服务开启执行代码
}
protected override void OnStop()
{
//服务结束执行代码
}
protected override void OnPause()
{
//服务暂停执行代码
base.OnPause();
}
protected override void OnContinue()
{
//服务恢复执行代码
base.OnContinue();
}
protected override void OnShutdown()
{
//系统即将关闭执行代码
base.OnShutdown();
}
全部代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Cjwdev.WindowsApi;

namespace process
{
public partial class Service1 : ServiceBase
{
string appStartPath = @”C:\Users\Administrator\Desktop\okl1\okl1.exe”;
public Service1()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
// string appStartPath= @”C:\Users\Administrator\Desktop\okl1\okl1.exe”;
System.Timers.Timer timer;

timer = new System.Timers.Timer();
timer.Interval = 10000;//设置计时器事件间隔执行时间
timer.Elapsed += new System.Timers.ElapsedEventHandler(circulation);
timer.Enabled = true;
}
protected override void OnStop()
{
}
private void circulation(object sender,System.Timers.ElapsedEventArgs e)
{
try
{
//appStartPath = “程序路径”;
IntPtr userTokenHandle = IntPtr.Zero;
ApiDefinitions.WTSQueryUserToken(ApiDefinitions.WTSGetActiveConsoleSessionId(), ref userTokenHandle);

ApiDefinitions.PROCESS_INFORMATION procInfo = new ApiDefinitions.PROCESS_INFORMATION();
ApiDefinitions.STARTUPINFO startInfo = new ApiDefinitions.STARTUPINFO();
startInfo.cb = (uint)System.Runtime.InteropServices.Marshal.SizeOf(startInfo);

ApiDefinitions.CreateProcessAsUser(
userTokenHandle,
appStartPath,
“”,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
null,
ref startInfo,
out procInfo);

if (userTokenHandle != IntPtr.Zero)
ApiDefinitions.CloseHandle(userTokenHandle);

int _currentAquariusProcessId = (int)procInfo.dwProcessId;
}
catch (Exception ex)
{

}
string appName = “okl1”;//the path of the exe file
bool runFlag = false;
Process[] myProcesses = Process.GetProcesses();
foreach (Process myProcess in myProcesses)
{
if (myProcess.ProcessName.CompareTo(appName) == 0)
{
runFlag = true;
}

}

if (!runFlag) //如果程序没有启动
{
Process proc = new Process();
proc.StartInfo.FileName = appName;
proc.StartInfo.WorkingDirectory = Path.GetDirectoryName(appStartPath);
proc.Start();

}
}
}
}
5.添加安装程序
在services1的设计界面右键,选择添加安装程序:
生成serviceInstaller1和 serviceProcessInstaller1两个组件 。

6.把serviceInstaller1的属性ServiceName改写为你的服务程序名,并把启动模式设置为AUTOMATIC

7.把serviceProcessInstaller1的属性account改写为 LocalSystem

8.通过从生成菜单中选择生成来生成项目
安装卸载Windows服务
现在你所需要的代码写完之后点击运行是运行不了的!!!

9.安装windows服务
1.InstallUtil.exe存在路径为:C:\WINDOWS\Microsoft.NET\Framework\.NET版本号\InstallUtil.exe

2.找到它把它复制到你的项目中的bin\Debug或者bin\Release下

3.打开cmd输入命令runas /user:Administrator cmd                     输入密码(不知道的自己百度吧)

4.获得更高权限,避免后续权限不够出问题

5.输入安装命令比如:C:\Windows\system32>xxx\xxx\bin\Debug\InstallUtil.exe 空格 服务名.exe

6.安装成功之后:控制面板>管理工具>计算机管理>服务和应用程序>服务>找到你的服务开启就ok了

卸载Windows服务
输入命令:C:\Windows\system32>xxx\xxx\bin\Debug\InstallUtil.exe 空格 服务名.exe/u     就是多了个/u

如果修改这个服务,但是路径没有变化的话是不需要重新注册服务的,直接停止服务,然后用新的文件覆盖原来的文件即可,如果路径发生变化,应该先卸载这个服务,然后重新安装这个服务。
————————————————
版权声明:本文为CSDN博主「Struggle_Cxg」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/struggle_cxg/article/details/83302251

C#邮件发送 - @大龙哥 - 博客园

mikel阅读(676)

来源: C#邮件发送 – @大龙哥 – 博客园

  这篇文章主要介绍如何使用C#的MailAddress类进行邮件的发送。

  1.首先引入命名空间using System.Net.Mail;

  2.将发送的邮件的功能封装成一个类,该类中包含了发送邮件的基本功能:收件人(多人),抄送(多人),发送人,主题,邮件正文,附件等,封装的Email类如下:

复制代码
 public class Email
    {
        /// <summary>
        /// 发送者
        /// </summary>
        public string mailFrom { get; set; }

        /// <summary>
        /// 收件人
        /// </summary>
        public string[] mailToArray { get; set; }

        /// <summary>
        /// 抄送
        /// </summary>
        public string[] mailCcArray { get; set; }

        /// <summary>
        /// 标题
        /// </summary>
        public string mailSubject { get; set; }

        /// <summary>
        /// 正文
        /// </summary>
        public string mailBody { get; set; }

        /// <summary>
        /// 发件人密码
        /// </summary>
        public string mailPwd { get; set; }

        /// <summary>
        /// SMTP邮件服务器
        /// </summary>
        public string host { get; set; }

        /// <summary>
        /// 正文是否是html格式
        /// </summary>
        public bool isbodyHtml { get; set; }

        /// <summary>
        /// 附件
        /// </summary>
        public string[] attachmentsPath { get; set; }

        public bool Send()
        {
            //使用指定的邮件地址初始化MailAddress实例
            MailAddress maddr = new MailAddress(mailFrom);
            //初始化MailMessage实例
            MailMessage myMail = new MailMessage();


            //向收件人地址集合添加邮件地址
            if (mailToArray != null)
            {
                for (int i = 0; i < mailToArray.Length; i++)
                {
                    myMail.To.Add(mailToArray[i].ToString());
                }
            }

            //向抄送收件人地址集合添加邮件地址
            if (mailCcArray != null)
            {
                for (int i = 0; i < mailCcArray.Length; i++)
                {
                    myMail.CC.Add(mailCcArray[i].ToString());
                }
            }
            //发件人地址
            myMail.From = maddr;

            //电子邮件的标题
            myMail.Subject = mailSubject;

            //电子邮件的主题内容使用的编码
            myMail.SubjectEncoding = Encoding.UTF8;

            //电子邮件正文
            myMail.Body = mailBody;

            //电子邮件正文的编码
            myMail.BodyEncoding = Encoding.Default;

            myMail.Priority = MailPriority.High;

            myMail.IsBodyHtml = isbodyHtml;

            //在有附件的情况下添加附件
            try
            {
                if (attachmentsPath != null && attachmentsPath.Length > 0)
                {
                    Attachment attachFile = null;
                    foreach (string path in attachmentsPath)
                    {
                        attachFile = new Attachment(path);
                        myMail.Attachments.Add(attachFile);
                    }
                }
            }
            catch (Exception err)
            {
                throw new Exception("在添加附件时有错误:" + err);
            }

            SmtpClient smtp = new SmtpClient();
            //指定发件人的邮件地址和密码以验证发件人身份
            smtp.Credentials = new System.Net.NetworkCredential(mailFrom, mailPwd);


            //设置SMTP邮件服务器
            smtp.Host = host;

            try
            {
                //将邮件发送到SMTP邮件服务器
                smtp.Send(myMail);
                return true;

            }
            catch (System.Net.Mail.SmtpException ex)
            {
                return false;
            }

        }
    }
复制代码

  3.页面调用发送邮件的类

复制代码
protected void Send_Click(object sender, EventArgs e)
        {
            Email email = new Email();
            email.mailFrom = "发送人的邮箱地址";
            email.mailPwd = "发送人邮箱的密码";
            email.mailSubject = "邮件主题";
            email.mailBody = "邮件内容";
            email.isbodyHtml = true;    //是否是HTML
            email.host = "smtp.126.com";//如果是QQ邮箱则:smtp:qq.com,依次类推
            email.mailToArray = new string[] { "******@qq.com","12345678@qq.com"};//接收者邮件集合
            email.mailCcArray = new string[] { "******@qq.com" };//抄送者邮件集合
            if (email.Send())
            {
                Response.Write("<script type='text/javascript'>alert('发送成功!');history.go(-1)</script>");//发送成功则提示返回当前页面;

            }
            else
            {
                Response.Write("<script type='text/javascript'>alert('发送失败!');history.go(-1)</script>");
            }
        }
复制代码

  以上就完成了在C#中如何发送邮件。

  点击下载Demo

九种高性能可用高并发的技术架构 - 萌萌丶小魔王 - 博客园

mikel阅读(557)

来源: 九种高性能可用高并发的技术架构 – 萌萌丶小魔王 – 博客园

分层架构是逻辑上的,在物理部署上,三层架构可以部署在同一个物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,即三层结构分别部署在不同的服务器上,是网站拥有更多的计算资源以应对越来越多的用户访问。

 1、分层

分层是企业应用系统中最常见的一种架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和维度组成一个完整的系统。

在网站的分层架构中,常见的为3层,即应用层、服务层、数据层。应用层具体负责业务和视图的展示;服务层为应用层提供服务支持;数据库提供数据存储访问服务,如数据库、缓存、文件、搜索引擎等。

分层架构是逻辑上的,在物理部署上,三层架构可以部署在同一个物理机上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,即三层结构分别部署在不同的服务器上,使网站拥有更多的计算资源以应对越来越多的用户访问。

所以,虽然分层架构模式最初的目的是规划软件清晰的逻辑结构以便于开发维护,但在网站的发展过程中,分层结构对网站支持高并发向分布式方向的发展至关重要。

 

2、冗余

网站需要7*24小时连续运行,那么就得有相应的冗余机制,以防止某台机器宕掉时无法访问,而冗余则可以通过部署至少两台服务器构成一个集群实现服务高可用。数据库除了定期备份还需要实现冷热备份。甚至可以在全球范围内部署灾备数据中心。

3、分离

如果说分层将软件横向方面进行切分,那么分隔就是在纵向方面对软件进行切分。网站越大,功能越复杂,服务和数据处理的种类也越多,将这些不同的功能和服务分离开来,包装成高内聚低耦合的模块单元,不仅有助于软件的开发维护,也便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。

大型网站分隔的粒度可能会很小。比如在应用层,将不同业务进行分隔,例如将购物、论坛、搜索、广告分隔成不同的应用,有独立的团队负责,部署在不同的服务器上。

4、异步

使用异步,业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方法异步执行进行协作。

具体实现则在单一服务器内部可用通过多线程共享内存的方式处理。在分布式系统中,可用通过分布式消息队列来实现异步。

异步架构的典型就是生产者消费者方式,两者不存在直接调用。

5、分布式

对于大型网站,分层和分隔的一个主要目的时为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作。分布式以为着可以使用更多的计算机完成同样的工作,计算机越多,CPU、内存、存储资源就越多,能处理的高并发访问和数据量就越大,进而能够为更多的用户提供服务。

在网站应用中,常用的分布式方案有以下几种:

分布式应用和服务:将分层和分隔后的应用和服务模块分布式部署,可以改善网络性能和并发性、加快开发和发布速度、减少数据库连接资源消耗。

分布式静态资源:网站的静态资源如JS、CSS、logo图片等资源独立分布部署,并采用独立的域名,即人们长多的动静分离。静态资源分布式部署可以减轻应用服务器的负载压力;通过使用独立域名加快浏览器并发加载速度。

分布式数据和存储:大型网站需要处理以P为单位的海量数据,单台计算机无法提供如此大的存储控件,这些数据需要分布式存储。

分布式计算:目前网站普遍使用Hadoop和MapReduce分布式计算框架进行此类批处理计算,其特点是移动计算而不是移动数据,将计算程序分发到数据所在的位置以加速计算和分布式计算。

6、安全

网站在安全架构方面有许多模式:通过密码和手机校验码进行身份认证;登录、交以需要对网络通信进行加密;为了防止机器人程序滥用资源,需要使用验证码进行识别;对常见的XSS工即、SQL注入需要编码转换;垃圾信息需要过滤等。

7、自动化

具体由自动化发布过程,自动化代码管理、自动化测试、自动化安全检测、自动化部署、自动化监控、自动化报警、自动化失效转移、自动化失效恢复等。

8、集群

对于用户访问集中的模块需要将独立部署的服务器集群化,即多台服务器部署相同的应用够成一个集群,通过负载均衡设备共同对外提供服务。

服务器集群能够为相同的服务提供更多的并发支持,因此当有更多的用户访问时,只需要向集群中加入新的机器即可;另外可以实现当其中的某台服务器发生故障时,可以通过负载均衡的失效转移机制将请求转移至集群中其他的服务器上,因此可以提高系统的可用性。

 

 9、缓存

缓存目的就是减轻服务器的计算,使数据直接返回给用户,在现在的软件设计中,缓存已经无处不再,具体实现有CDN、反向代理、本地缓存、分布式缓存等。

使用缓存有两个条件:访问数据热点不均衡,即某些频繁访问的数据需要放在缓存中;数据在某个时间段内有效,不会很快过期,不会因为数据过期而脏读,影响数据的正确性。

 

 

以上为总结的9中高可用框架。

努力,不是为了要感动谁,也不是要做给哪个人看,而是要让自己随时有能力跳出自己厌恶的圈子,并拥有选择的权利。记住,用自己喜欢的方式过一生。

谈谈分布式事务之三: System.Transactions事务详解[上篇] - Artech - 博客园

mikel阅读(622)

来源: 谈谈分布式事务之三: System.Transactions事务详解[上篇] – Artech – 博客园

在.NET 1.x中,我们基本是通过ADO.NET实现对不同数据库访问的事务。.NET 2.0为了带来了全新的事务编程模式,由于所有事务组件或者类型均定义在System.Transactions程序集中的System.Transactions命名空间下,我们直接称基于此的事务为System.Transactions事务。System.Transactions事务编程模型使我们可以显式(通过System.Transactions.Transaction)或者隐式(基于System.Transactions.TransactionScope)的方式进行事务编程。我们先来看看,这种全新的事务如何表示。

一、System.Transactions.Transaction

在System.Transactions事务体系下,事务本身通过类型System.Transactions.Transaction类型表示,下面是Transaction的定义:

   1: [Serializable]
   2: public class Transaction : IDisposable, ISerializable
   3: {
   4:     public event TransactionCompletedEventHandler TransactionCompleted;
   5:
   6:     public Transaction Clone();
   7:     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
   8:
   9:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  10:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);
  11:     public bool EnlistPromotableSinglePhase(IPromotableSinglePhaseNotification promotableSinglePhaseNotification);
  12:     public Enlistment EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  13:     public Enlistment EnlistVolatile(ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);
  14:
  15:     public void Rollback();
  16:     public void Rollback(Exception e);
  17:
  18:     void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext context);
  19:
  20:     public static Transaction Current { get; set; }
  21:
  22:     public IsolationLevel IsolationLevel { get; }
  23:     public TransactionInformation TransactionInformation { get; }
  24: }

1、Transaction是可序列化的

从上面的定义我们可以看到,Transaction类型(在没有特殊说明的情况下,以下的Transaction类型指的就是System.Transactions.Transaction)上面应用的SerializableAttribute特性,并且实现了ISerializable接口,意味着一个Transaction对象是可以被序列化的。Transaction的这一特性在WCF整个分布式事务的实现意义重大,原因很简单:要让事务能够控制整个服务操作,必须实现事务的传播,而传播的前提就是事务可被序列化

2、如何登记事务参与者

Transaction中,定义了五个EnlistXxx方法用于将涉及到的资源管理器登记到当前事务中。其中EnlistDurable和EnlistVolatile分别实现了对持久化资源管理器和易失资源管管理器的事务登记,而EnlistPromotableSinglePhase则针对的是可被提升的资源管理器(比如基于SQL Server 2005和SQL Server 2008)。

事务登记的目的是建立事务提交树,使得处于根节点的事务管理器能够在事务提交的时候能够沿着这棵树将相应的通知发送给所有的事务参与者。这种至上而下的通知机制依赖于具体采用事务提交协议,或者说某个资源要求参与到当前事务之中,必须满足基于协议需要的接收和处理相应通知的能力。System.Transactions将不同事务提交协议对参与者的要求定义在相应的接口中。其中IEnlistmentNotificationISinglePhaseNotification分别是基于2PC和SPC(关于2PC和SPC,在上篇中有详细的介绍)。

如果我们需要为相应的资源开发能够参与到System.Transactions事务的资源管理器,需要事先实现IEnlistmentNotification接口,对基本的2PC协议提供支持。当满足SPC要求的时候,如果希望采用SPC优化协议,则需要实现ISinglePhaseNotification接口。如果希望像SQL Server 2005或者SQL Server 2008支持事务提升机制,则需要实现IPromotableSinglePhaseNotification接口。

3、环境事务(Ambient Transaction)

Transaction定义了一个类型为Transaction的Current静态属性(可读可写),表示当前的事务。作为当前事务的Transaction存储于当前线程的TLS(Thread Local Storage)中(实际上是定义在一个应用了ThreadStaticAttribute特性的静态字段上),所以仅对当前线程有效。如果进行异步调用,当前事务并不能自动事先跨线程传播,将异步操作纳入到当前事务,需要使用到另外一个事务:依赖事务。

这种基于当前线程的当前事务又称环境事务(Ambient Transaction),很多资源管理器都具有对环境事务的感知能力。也就是说,如果我们通过Current属性设置了环境事务,当对某个具有环境事务感知能力的资源管理器进行访问的时候,相应的资源管理器会自动登记到当前事务中来。我们将具有这种感知能力的资源管理器称为System.Transactions资源管理器。

4、事务标识

Transaction具有一个只读的TransactionInformation属性,表示事务一些基本的信息。属性的类型为TransactionInformation,定义如下:

   1: public class TransactionInformation
   2: {
   3:     public DateTime CreationTime { get; }
   4:     public TransactionStatus Status { get; }
   5:
   6:     public string LocalIdentifier { get; }
   7:     public Guid DistributedIdentifier { get; }
   8: }

TransactionInformation的CreationTime和Status表示创建事务的时间和事务的当前状态。事务具有活动(Active)、提交(Committed)、中止(Aborted)和未决(In-Doubt)四种状态,通过TransactionStatus枚举表示。

   1: public enum TransactionStatus
   2: {
   3:     Active,
   4:     Committed,
   5:     Aborted,
   6:     InDoubt
   7: }

事务具有两个标识符,一个是本地标识,另一个是分布式标识,分别通过TransactionInformation的只读属性LocalIdentifier和DistributedIdentifier表示。本地标识由两部分组成:标识为本地应用程序域分配的轻量级事务管理器(LTM)的GUID和一个递增的整数(表示当前LMT管理的事务序号)。在下面的代码中,我们分别打印出三个新创建的可提交事务(CommittableTransaction,为Transaction的子类,我们后面会详细介绍)的本地标识。

   1: using System;
   2: using System.Transactions;
   3: class Proggram
   4: {
   5:     static void Main()
   6:     {
   7:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   8:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   9:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
  10:     }
  11: }

输出结果:

AC48F192-4410-45fe-AFDC-8A890A3F5634:1
AC48F192-4410-45fe-AFDC-8A890A3F5634:2
AC48F192-4410-45fe-AFDC-8A890A3F5634:3

一旦本地事务提升到基于DTC的分布式事务,系统会为之生成一个GUID作为其唯一标识。当事务跨边界执行的时候,分布式事务标识会随着事务一并被传播,所以在不同的执行上下文中,你会得到相同的GUID。分布式事务标识通过TransactionInformation的只读属性DistributedIdentifier表示,我经常在审核(Audit)中使用该标识。

对于上面Transaction的介绍,细心的读者可能会发现两个问题:Transaction并没有提供公有的构造函数,意味着我们不能直接通过new操作符创建Transaction对象;Transaction只有两个重载的Rollback方法,并没有Commit方法,意味着我们直接通过Transaction进行事务提交。

在一个分布式事务中,事务初始化和提交只能有相同的参与者担当。也就是说只有被最初开始的事务才能被提交,我们将这种能被初始化和提交的事务称作可提交事务(Committable Transaction)。随着分布式事务参与者逐个登记到事务之中,它们本地的事务实际上依赖着这个最初开始的事务,所以我们称这种事务为依赖事务(Dependent Transaction)。

二、 可提交事务(CommittableTransaction)

只有可提交事务才能被直接初始化,对可提交事务的提交驱动着对整个分布式事务的提交。可提交事务通过CommittableTransaction类型表示。照例先来看看CommittableTransaction的定义:

   1: [Serializable]
   2: public sealed class CommittableTransaction : Transaction, IAsyncResult
   3: {
   4:     public CommittableTransaction();
   5:     public CommittableTransaction(TimeSpan timeout);
   6:     public CommittableTransaction(TransactionOptions options);
   7:
   8:     public void Commit();
   9:     public IAsyncResult BeginCommit(AsyncCallback asyncCallback, object asyncState);
  10:     public void EndCommit(IAsyncResult asyncResult);
  11:
  12:     object IAsyncResult.AsyncState { get; }
  13:     WaitHandle IAsyncResult.AsyncWaitHandle { get; }
  14:     bool IAsyncResult.CompletedSynchronously { get; }
  15:     bool IAsyncResult.IsCompleted { get; }
  16: }

1、可提交事务的超时时限和隔离级别

CommittableTransaction直接继承自Transaction,提供了三个公有的构造函数。通过TimeSpan类型的timeout参数指定事务的超时实现,自被初始化那一刻开始算起,一旦超过了该时限,事务会被中止。通过TransactionOptions类型的options可以同时指定事务的超时时限和隔离级别。TransactionOptions是一个定义在System.Transactions命名空间下的结构(Struct),定义如下,两个属性Timeout和IsolationLevel分别代表事务的超时时限和隔离级别。

   1: [StructLayout(LayoutKind.Sequential)]
   2: public struct TransactionOptions
   3: {
   4:     //其他成员
   5:     public TimeSpan Timeout { get; set; }
   6:     public IsolationLevel IsolationLevel { get; set; }
   7: }

如果调用默认无参的构造函数来创建CommittableTransaction对象,意味着采用一个默认的超时时限。这个默认的时间是1分钟,不过可以它可以通过配置的方式进行指定。事务超时时限相关的参数定义在<system.transactions>配置节中,下面的XML体现的是默认的配置。从该段配置我们可以看到,我们不但可以通过<defaultSettings>设置事务默认的超时时限,还可以通过<machineSettings>设置最高可被允许的事务超时时限,默认为10分钟。在对这两项进行配置的时候,前者的时间必须小于后者,否则将用后者作为事务默认的超时时限。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.transactions>
   4:     <defaultSettings timeout="00:01:00"/>
   5:     <machineSettings maxTimeout="00:10:00"/>
   6:   </system.transactions>
   7: </configuration>

作为事务ACID四大属性之一的隔离性(Isolation),确保事务操作的中间状态的可见性仅限于事务内部。隔离机制通过对访问的数据进行加锁,防止数据被事务的外部程序操作,从而确保了数据的一致性。但是隔离机制在另一方面又约束了对数据的并发操作,降低数据操作的整体性能。为了权衡着两个互相矛盾的两个方面,我们可以根据具体的情况选择相应的隔离级别。

在System.Transactions事务体系中,为事务提供了7种不同的隔离级别。这7中隔离级别分别通过System.Transactions.IsolationLevel的7个枚举项表示。

   1: public enum IsolationLevel
   2: {
   3:     Serializable,
   4:     RepeatableRead,
   5:     ReadCommitted,
   6:     ReadUncommitted,
   7:     Snapshot,
   8:     Chaos,
   9:     Unspecified
  10: }

7个隔离级别之中,Serializable具有最高隔离级别,代表的是一种完全基于序列化(同步)的数据存取方式,这也是System.Transactions事务默认采用的隔离级别。按照隔离级别至高向低,7个不同的隔离级别代表的含义如下:

  • Serializable:可以在事务期间读取可变数据,但是不可以修改,也不可以添加任何新数据;
  • RepeatableRead:可以在事务期间读取可变数据,但是不可以修改。可以在事务期间添加新数据;
  • ReadCommitted:不可以在事务期间读取可变数据,但是可以修改它;
  • ReadUncommitted:可以在事务期间读取和修改可变数据;
  • Snapshot:可以读取可变数据。在事务修改数据之前,它验证在它最初读取数据之后另一个事务是否更改过这些数据。如果数据已被更新,则会引发错误。这样使事务可获取先前提交的数据值;
  • Chaos:无法覆盖隔离级别更高的事务中的挂起的更改;
  • Unspecified:正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。如果设置了此值,则会引发异常。

2、事务的提交

CommittableTransaction提供了同步(通过Commit方法)和异步(通过BeginCommit|EndCommit方法组合)对事务的提交。此外CommittableTransaction还是实现了IAsyncResult这么一个接口,如果采用异步的方式调用BeginCommit方法提交事务,方法返回的IAsyncResult对象的各属性值会反映在CommittableTransaction同名属性上面。

前面我们提到了环境事务已经System.Transactions资源管理器对环境事务的自动感知能力。当创建了CommittableTransaction对象的时候,被创建的事务并不会自动作为环境事务,你需要手工将其指定到Transaction的静态Current属性中。接下来,我们将通过一个简单的例子演示如果通过CommittableTransaction实现一个分布式事务。

3、实例演示:通过CommittableTransaction实现分布式事务

在这个实例演示中,我们沿用介绍事务显式控制时使用到的银行转帐的场景,并且直接使用第一篇中创建的帐户表(T_ACCOUNT)。一个完整的转帐操作本质上有两个子操作完成,提取和存储,即从一个帐户中提取相应的金额存入另一个帐户。为了完成这两个操作,我写了如下两个存储过程:P_WITHDRAW和P_DEPOSIT。

P_WITHDRAW:

   1: CREATE Procedure P_WITHDRAW
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id)
   8:     BEGIN
   9:         RAISERROR ('帐户ID不存在',16,1)
  10:         RETURN
  11:     END
  12: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id AND BALANCE > @amount)
  13:     BEGIN
  14:         RAISERROR ('余额不足',16,1)
  15:         RETURN
  16:     END
  17:
  18: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance - @amount WHERE Id = @id
  19: GO

P_DEPOSIT:

   1: CREATE Procedure P_DEPOSIT
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   8:     BEGIN
   9:         RAISERROR ('帐户ID不存在',16,1)
  10:     END
  11: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance + @amount WHERE Id = @id
  12: GO

为了确定是否成功转帐,我们需要提取相应帐户的当前余额,我们相应操作实现在下面一个存储过程中。

   1: CREATE Procedure P_GET_BALANCE_BY_ID
   2:     (
   3:         @id VARCHAR(50)
   4:     )
   5: AS
   6: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   7:     BEGIN
   8:         RAISERROR ('帐户ID不存在',16,1)
   9:     END
  10: SELECT BALANCE FROM [dbo].[T_ACCOUNT] WHERE Id = @id
  11: GO

为了执行存储过程的方便,我写了一个简单的工具类DbAccessUtil。ExecuteNonQuery和ExecuteScalar的作用于DbCommand同名方法相同。使用DbAccessUtil的这两个方法,只需要以字符串和字典的方式传入存储过程名称和参数即可。由于篇幅所限,关于具有实现不再多做介绍了,又兴趣的读者,可以参考《WCF技术剖析(卷1)》的最后一章,里面的DbHelper提供了相似的实现。

   1: public static class DbAccessUtil
   2: {
   3:     public static int ExecuteNonQuery(string procedureName, IDictionary<string, object> parameters);
   4:     public static T ExecuteScalar<T>(string procedureName, IDictionary<string, object> parameters);
   5: }

借助于DbAccessUtil提供的辅助方法,我们定义两个方法Withdraw和Deposit分别实现提取和存储的操作,已近获取某个帐户当前余额。

   1: static void Withdraw(string accountId, double amount)
   2: {
   3:     Dictionary<string, object> parameters = new Dictionary<string, object>();
   4:     parameters.Add("id", accountId);
   5:     parameters.Add("amount", amount);
   6:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
   7: }
   8: static void Deposite(string accountId, double amount)
   9: {
  10:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  11:     parameters.Add("id", accountId);
  12:     parameters.Add("amount", amount);
  13:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
  14: }
  15: private static double GetBalance(string accountId)
  16: {
  17:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  18:     parameters.Add("id", accountId);
  19:     return DbAccessUtil.ExecuteScalar<double>("P_GET_BALANCE_BY_ID", parameters);
  20: }

现在假设帐户表中有一个帐号,它们的ID分别为Foo,余额为5000。下面是没有采用事务机制的转帐实现(注意:需要转入的帐户不存在)。

   1: using System;
   2: using System.Collections.Generic;
   3: namespace Artech.TransactionDemo
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             string accountFoo = "Foo";
  10:             string nonExistentAccount = Guid.NewGuid().ToString();
  11:             //输出转帐之前的余额
  12:             Console.WriteLine("帐户\"{0}\"的当前余额为:¥{1}", accountFoo, GetBalance(accountFoo));
  13:             //开始转帐
  14:             try
  15:             {
  16:                 Transfer(accountFoo, nonExistentAccount, 1000);
  17:             }
  18:             catch (Exception ex)
  19:             {
  20:                 Console.WriteLine("转帐失败,错误信息:{0}", ex.Message);
  21:             }
  22:             //输出转帐后的余额
  23:             Console.WriteLine("帐户\"{0}\"的当前余额为:¥{1}", accountFoo, GetBalance(accountFoo));
  24:         }
  25:
  26:         private static void Transfer(string accountFrom, string accountTo, double amount)
  27:         {
  28:             Withdraw(accountFrom, amount);
  29:             Deposite(accountTo, amount);
  30:         }
  31:     }
  32: }

输出结果:

帐户"Foo"的当前余额为:¥5000
转帐失败,错误信息:帐户ID不存在
帐户"Foo"的当前余额为:¥4000

由于没有采用事务,在转入帐户根本不存在情况下,款项依然被转出帐户提取出来。现在我们通过CommittableTransaction将整个转帐操作纳入同一个事务中,只需要将Transfer方法进行如下的改写:

   1: private static void Transfer(string accountFrom, string accountTo, double amount)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction transaction = new CommittableTransaction();
   5:     try
   6:     {
   7:         Transaction.Current = transaction;
   8:         Withdraw(accountFrom, amount);
   9:         Deposite(accountTo, amount);
  10:         transaction.Commit();
  11:     }
  12:     catch (Exception ex)
  13:     {
  14:         transaction.Rollback(ex);
  15:         throw;
  16:     }
  17:     finally
  18:     {
  19:         Transaction.Current = originalTransaction;
  20:         transaction.Dispose();
  21:     }
  22: }

输出结果(将余额恢复成5000):

帐户"Foo"的当前余额为:¥5000
转帐失败,错误信息:帐户ID不存在
帐户"Foo"的当前余额为:¥5000

下一篇中我们将重点介绍DependentTransactionTransactionScope

 

分布式事务系列:
谈谈分布式事务之一:SOA需要怎样的事务控制方式
谈谈分布式事务之二:基于DTC的分布式事务管理模型[上篇]
谈谈分布式事务之二:基于DTC的分布式事务管理模型[下篇]
谈谈分布式事务之三: System.Transactions事务详解[上篇]
谈谈分布式事务之三: System.Transactions事务详解[下篇]

谈谈分布式事务之三: System.Transactions事务详解[下篇] - Artech - 博客园

mikel阅读(596)

来源: 谈谈分布式事务之三: System.Transactions事务详解[下篇] – Artech – 博客园

前面一篇给出的Transaction的定义中,信息的读者应该看到了一个叫做DepedentClone的方法。该方法对用于创建基于现有Transaction对象的“依赖事务(DependentTransaction)”。不像可提交事务是一个独立的事务对象,依赖事务依附于现有的某个事务(可能是可提交事务,也可能是依赖事务)。依赖事务可以帮助我们很容易地编写一些事务型操作,当环境事务不存的时候,可以确保操作在一个独立的事务中执行;当环境事务存在的时候,则自动加入其中。

一、依赖事务(Dependent Transaction)

依赖事务通过DependentTransaction类型表示,DependentTransaction定义如下。和CommittableTransaction一样,DependentTransaction也是Transaction的子类。既然DependentTransaction依赖于现有的Transaction对象而存在,相当于被依赖事务的子事务,所以无法执行对事务的提交,也自然不会定义Commit方法。但是,DependentTransaction具有一个唯一的方法成员:Complete。调用这个方法意味着向被依赖事务发送通知,表明所有与依赖事务相关的操作已经完成。

   1: [Serializable]
   2: public sealed class DependentTransaction : Transaction
   3: {
   4:     public void Complete();
   5: }

1、通过DependentTransaction将异步操所纳入现有事务

通过Transaction的静态属性Current表示的环境事务保存在TLS(Thread Local Storage)中,所以环境事务是基于当前线程的。这就意味着,即使环境事务存在,通过异步调用的操作也不可能自动加入到当前事务之中,因为在异步线程中感知不到环境事务的存在。在这种情况下,我们需要做的就是手工将当前事务传递到另一个线程中,作为它的环境事务。通过依赖事务我们很容易实现这一点。

DependentTransaction通过Transaction的DependentClone方法创建,该方法具有一个DependentCloneOption枚举类型的参数,体现了被依赖的事务再上尚未接受到依赖事务的通知(调用Complete或者Rollback方法)得情况下,提交或者完成所采取的事务控制行为。DependentCloneOption提供了两个选项,BlockCommitUntilComplete表示被依赖事务会一直等待接收到依赖事务的通知或者超过事务设定的超时时限;而RollbackIfNotComplete则会直接将被依赖的事务回滚,并抛出TransactionAbortedException异常。

   1: [Serializable]
   2: public class Transaction : IDisposable, ISerializable
   3: {
   4:     //其他成员
   5:     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
   6: }
   7: public enum DependentCloneOption
   8: {
   9:     BlockCommitUntilComplete,
  10:     RollbackIfNotComplete
  11: }

下面的代码演示了如果通过依赖事务,采用异步的方式进行银行转账操作。借助于组件ThreadPool,将主线程环境事务的依赖事务传递给异步操作代理,开始异步操作的时候将此依赖事务作为当前的环境事务,那么之后的操作将自动在当前事务下进行。

   1: private static void Transfer(string accountFrom, string accountTo, double amount)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction transaction = new CommittableTransaction();
   5:     try
   6:     {
   7:         Transaction.Current = transaction;
   8:         ThreadPool.QueueUserWorkItem(state =>
   9:         {
  10:             Transaction.Current = state as DependentTransaction;
  11:             try
  12:             {
  13:                 Withdraw(accountFrom, amount);
  14:                 Deposite(accountTo, amount);
  15:                 (state as DependentTransaction).Complete();
  16:             }
  17:             catch (Exception ex)
  18:             {
  19:                 Transaction.Current.Rollback(ex);
  20:             }
  21:             finally
  22:             {
  23:                 (state as IDisposable).Dispose();
  24:                 Transaction.Current = null;
  25:             }
  26:         }, Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete));
  27:         //其他操作
  28:         transaction.Commit();
  29:     }
  30:     catch (TransactionAbortedException ex)
  31:     {
  32:         transaction.Rollback(ex);
  33:         Console.WriteLine("转帐失败,错误信息:{0}", ex.InnerException.Message);
  34:     }
  35:     catch (Exception ex)
  36:     {
  37:         transaction.Rollback(ex);
  38:         throw;
  39:     }
  40:     finally
  41:     {
  42:         Transaction.Current = originalTransaction;
  43:         transaction.Dispose();
  44:     }
  45: }

由于在调用DependentClone方法创建依赖事务时指定的参数为DependentCloneOption.BlockCommitUntilComplete,所以主线程在调用Commit方法提交事务的时候,由于依赖事务尚未结束(调用Complete或者Rollback方法),在这里会一直等待。如果依赖事务的Complete或者Rollback一直没有调用,那么被依赖的事务会一直等到超出事务设置的超时时限。所以,对于基于BlockCommitUntilComplete选项创建的依赖事务来说,应该及时地调用Complete或者Rollback方法。

2、通过DependentTransaction实现事务型方法

这里所说的事务型方法是指方法的执行总是在事务中执行。具体来讲,有两种不同的事务应用场景:如果当前不存在环境事务,那么方法的执行将在一个独立的事务中执行;反之,如果存在环境事务,在方法执行会自动加入到环境事务之中。

比如说,存储(Deposit)和提取(Withdraw)就是典型的事务型操作。对于单纯的存取款的场景,应该创建一个新的事务来控制存储和提取操作的执行,以确保单一帐户款项的数据一致性。如果在转账的场景中,应在在转账开始之前就创建一个新的事务,让提取和存储的操作自动加入到这个事务之中。

我们现在就结合可提交事务和依赖事务将Deposit和Withdraw两个方法定义成事务型方法,为了相同代码的重复,在这里把事务控制部分定义在如下一个InvokeInTransaction静态方法中:

   1: static void InvokeInTransaction(Action action)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction committableTransaction = null;
   5:     DependentTransaction dependentTransaction = null;
   6:     if (null == Transaction.Current)
   7:     {
   8:         committableTransaction = new CommittableTransaction();
   9:         Transaction.Current = committableTransaction;
  10:     }
  11:     else
  12:     {
  13:         dependentTransaction = Transaction.Current.DependentClone(DependentCloneOption.RollbackIfNotComplete);
  14:         Transaction.Current = dependentTransaction;
  15:     }
  16:
  17:     try
  18:     {
  19:         action();
  20:         if (null != committableTransaction)
  21:         {
  22:             committableTransaction.Commit();
  23:         }
  24:
  25:         if (null != dependentTransaction)
  26:         {
  27:             dependentTransaction.Complete();
  28:         }
  29:     }
  30:     catch (Exception ex)
  31:     {
  32:         Transaction.Current.Rollback(ex);
  33:         throw;
  34:     }
  35:     finally
  36:     {
  37:         Transaction transaction = Transaction.Current;
  38:         Transaction.Current = originalTransaction;
  39:         transaction.Dispose();
  40:     }
  41: }

InvokeInTransaction方法的参数是一个Action类型的代理(Delegate),表示具体的业务操作。在开始的时候记录下当前的环境事务,当整个操作结束之后应该环境事务恢复成该值。如果存在环境事务,则创建环境事务的依赖事务,反之直接创建可提交事务。并将新创建的依赖事务或者可提交事务作为当前的环境事务。将目标操作的执行(action)放在try/catch中,当目标操作顺利执行后,调用依赖事务的Complete方法或者可提交事务的Commit方法。如果抛出异常,则调用环境事务的Rollback进行回滚。在finally块中将环境事务恢复到之前的状态,并调用Dispose方法对创建的事务进行回收。

借助于InvokeInTransaction这个辅助方法,我们以事务型方法的形式定义了如下的两个方法:Withdraw和Deposit,分别实现提取和存储的操作。

   1: static void Withdraw(string accountId, double amount)
   2: {
   3:     Dictionary<string, object> parameters = new Dictionary<string, object>();
   4:     parameters.Add("id", accountId);
   5:     parameters.Add("amount", amount);
   6:     InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_WITHDRAW", parameters));
   7: }
   8:
   9: static void Deposit(string accountId, double amount)
  10: {
  11:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  12:     parameters.Add("id", accountId);
  13:     parameters.Add("amount", amount);
  14:     InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters));
  15: }

二、TransactionScope

在上面一节,我结合可提交事务和依赖事务,以及环境事务的机制提供了对事务型操作的实现。实际上,如果借助TransactionScope,相应的代码将会变得非常简单。下面的代码中,通过TransactionScope对InvokeInTransaction进行了改写,从执行效果来看这和原来的代码完全一致。

   1: static void InvokeInTransaction(Action action)
   2: {
   3:     using (TransactionScope transactionScope = new TransactionScope())
   4:     {
   5:         action();
   6:         transactionScope.Complete();
   7:     }
   8: }

通过InvokeInTransaction方法前后代码的对比,我们可以明显看到TransactionScope确实能够使我们的事务控制变得非常的简单。实际上,在利用System.Transactions事务进行编程的时候,我们一般不会使用到可提交事务,对于依赖事务也只有在异步调用的时候会使用到,基于TransactionScope的事务编程方式才是我们推荐的。

正如其名称所表现的一样,TransactionScope就是为一组事务型操作创建一个执行范围,而这个范围始于TransactionScope创建之时,结束于TransactionScope被回收(调用Dispose方法)。在对TransactionScope进行深入介绍之前,照例先来看看它的定义:

   1: public sealed class TransactionScope : IDisposable
   2: {
   3:     public TransactionScope();
   4:     public TransactionScope(Transaction transactionToUse);
   5:     public TransactionScope(TransactionScopeOption scopeOption);
   6:     public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout);
   7:     public TransactionScope(TransactionScopeOption scopeOption, TimeSpan scopeTimeout);
   8:     public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions);
   9:     public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout, EnterpriseServicesInteropOption interopOption);
  10:     public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions, EnterpriseServicesInteropOption interopOption);
  11:
  12:     public void Complete();
  13:     public void Dispose();
  14: }

我们可以看到TransactionScope实现了IDisposable接口,除了Dispose方法之外,仅仅具有一个唯一的方法:Complete。但是TransactionScope却有一组丰富的构造函数。我们先来看看这些构造函数相应的参数如何影响TransactionScope对事务控制的行为。

1、TransactionScopeOption

实际上前面一节中提供的InvokeInTransaction方法基本上体现了TransactionScope的内部实现。也就是说,TransactionScope也是通过创建可提交事务或者依赖事务,并将其作为事务范围内的环境事务,从而将范围的所有操作纳入到一个事务之中。

通过在构造函数中指定TransactionScopeOption类型的scopeOption参数,控制TransactionScope当环境事务存在的时候应该采取怎样的方式执行事务范围内的操作。具有来讲,具有三种不同的方式:

  • 如果已经存在环境事务,则使用该环境事务。否则,在进入范围之前创建新的事务;
  • 总是为该范围创建新事务;
  • 环境事务上下文在创建范围时被取消。范围中的所有操作都在无环境事务上下文的情况下完成。

TransactionScopeOption是一个枚举,三个枚举值Required、RequiresNew和Suppress依次对应上面的三种行为。

   1: public enum TransactionScopeOption
   2: {
   3:     Required,
   4:     RequiresNew,
   5:     Suppress
   6: }

对于Required选项,如果当前存在环境事务TransactionScope会创建环境事务的依赖事务,负责创建可提交事务,然后将创建的环境事务或者可提交事务作为事务范围的环境事务。如对于RequiresNew选项,TransactionScope总是会创建可提交事务并将其作为事务范围的环境事务,意味着控制事务范围内操作的事务也当前的环境事务已经没有任何关系。如果Suppress选项,TransactionScope会将事务范围内的环境事务设为空,意味着事务范围内的操作并不受事务的控制。

Required是默认选项,意味着事务范围内的事务将会作为当前环境事务的一部分。如果你不希望某个操作被纳入当前的环境事务,但是相应的操作也需要事务的控制以确保所操作数据的一致性。比如,当业务逻辑失败导致异常抛出,需要对相应的错误信息进行日志记录。对于日记的操作就可以放入基于RequiresNew选项创建TransactionScope中。对于一些不重要的操作(操作的错误可被忽略),并且不需要通过事务来控制的操作,比如发送一些不太重要的通知,就可以采用Suppress选项。

2、TransactionOptions和EnterpriseServicesInteropOption

TransactionOptions在前面已经提及,用于控制事务的超时时限和隔离级别。对于超时时限,你也可以选择TransactionScope相应能够的构造函数以TimeSpan的形式指定。而对于事务的隔离级别,需要着重强调一点:当选择TransactionScopeOption.Required选项时,TransactionScope指定的隔离级别必须与环境事务(如果有)相匹配。

比如下面的例子中,我定义两个嵌套的TransactionScope,外部的TransactionScope采用默认的隔离级别,内部在采用ReadCommitted隔离级别,当执行这段代码的时候,会抛出如图1所示的ArgumentException异常。

   1: using (TransactionScope outerScope = new TransactionScope())
   2: {
   3:     TransactionOptions transactionOptions = new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted };
   4:     using (TransactionScope innerScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
   5:     {
   6:         //事务型操作
   7:         innerScope.Complete();
   8:     }
   9:     //事务型操作
  10:     outerScope.Complete();
  11: }

image

图1 隔离级别不一致导致的异常

实际上在System.Transactions事务机制被引入之前,像Enterprise Service主要依赖于基于COM+的分布式事务。TransactionScope通过EnterpriseServicesInteropOption控制System.Transactions事务如何与COM+的分布式事务进行互操作。具有来讲具有如下三种互操作选项,分别和EnterpriseServicesInteropOption三个枚举值相对应:

  • None:Transaction 和 Current 之间不同步;
  • Automatic:搜索现有的 COM+ 上下文并与之同步(如该上下文存在);
  • Full:System.EnterpriseServices 上下文(可通过调用 ContextUtil 类的静态方法 Transaction 来检索)和 System.Transactions 环境事务(可通过调用 Transaction 类的静态方法 Current 来检索)始终保持同步。这将引入性能损失,因为可能需要创建新的 System.EnterpriseServices 上下文。
   1: public enum EnterpriseServicesInteropOption
   2: {
   3:     None,
   4:     Automatic,
   5:     Full
   6: }

3、事务提交和回滚

对于事务范围中的事务,无论是事务的提交(对于可提交事务)、完成(依赖事务)和回滚都是在Dispose方法中执行的。TransactionScope中定一个个私有的布尔类型字段(complete)表示事务是否正常结束。该成员的默认值为False,当调用TransactionScope的Complete方法的时候会将此字段设置成True。当Dispose执行的时候,如果该字段的值为False,会调用事务的Rollback方法对该事务实施回滚;否则会调用Commit方法(对于可提交事务)对事务进行提交或者调用Complete方法(依赖事务)通知被依赖的事务本地事务已经正常完成。

除了执行事务的提交、完成或者回滚之外,TransactionScope的Dispose方法还负责将环境事务回复到事务范围开始之前的状态。在调用Complete和Dispose之前,环境事务处于不可用的状态,如果此时试图获取环境事务,会抛出异常。比如在下面的代码中,在事务范围内部调用Complete方法后,通过Transaction的Current静态属性获取当前环境事务,会抛出图2所示的InvalidOpertionException异常。

   1: using (TransactionScope transactionScope = new TransactionScope())
   2: {
   3:     //其他事务操作
   4:     transactionScope.Complete();
   5:     Transaction ambientTransaction = Transaction.Current;
   6: }

image 图2 在TransactionScope完成之后获取环境事务导致的异常

分布式事务系列:
谈谈分布式事务之一:SOA需要怎样的事务控制方式
谈谈分布式事务之二:基于DTC的分布式事务管理模型[上篇]
谈谈分布式事务之二:基于DTC的分布式事务管理模型[下篇]
谈谈分布式事务之三: System.Transactions事务详解[上篇]
谈谈分布式事务之三: System.Transactions事务详解[下篇]

30分钟全面解析-SQL事务+隔离级别+阻塞+死锁 - 悟空聊架构 - 博客园

mikel阅读(760)

来源: 30分钟全面解析-SQL事务+隔离级别+阻塞+死锁 – 悟空聊架构 – 博客园

以前总是追求新东西,发现基础才是最重要的,今年主要的目标是精通SQL查询和SQL性能优化。

 本系列主要是针对T-SQL的总结。

【T-SQL基础】01.单表查询-几道sql查询题

【T-SQL基础】02.联接查询

【T-SQL基础】03.子查询

【T-SQL基础】04.表表达式-上篇

【T-SQL基础】04.表表达式-下篇

【T-SQL基础】05.集合运算

【T-SQL基础】06.透视、逆透视、分组集

【T-SQL基础】07.数据修改

【T-SQL基础】08.事务和并发

【T-SQL基础】09.可编程对象

———————————————————-

【T-SQL进阶】01.好用的SQL TVP~~独家赠送[增-删-改-查]的例子

———————————————————-

【T-SQL性能调优】01.TempDB的使用和性能问题

【T-SQL性能调优】02.Transaction Log的使用和性能问题

【T-SQL性能调优】03.执行计划

【T-SQL性能调优】04.死锁分析

持续更新……欢迎关注我!

概述:

本篇主要是对SQL中事务和并发的详细讲解。

一、事务

1.什么是事务

为单个工作单元而执行的一系列操作。如查询、修改数据、修改数据定义。

2.语法

(1)显示定义事务的开始、提交

BEGIN TRAN
INSERT INTO b(t1) VALUES(1)
INSERT INTO b(t1) VALUES(2)
COMMIT TRAN

(2)隐式定义

如果不显示定义事务的边界,则SQL Server会默认把每个单独的语句作为一个事务,即在执行完每个语句之后就会自动提交事务。

3.事务的四个属性ACID

(1)原子性Atomicity

1.事务必须是原子工作单元。事务中进行的修改,要么全部执行,要么全都不执行;

2.在事务完成之前(提交指令被记录到事务日志之前),系统出现故障或重新启动,SQL Server将会撤销在事务中进行的所有修改;

3.事务在处理中遇到错误,SQL Server通常会自动回滚事务;

4.少数不太严重的错误不会引发事务的自动回滚,如主键冲突、锁超时等;

5.可以使用错误处理来捕获第4点提到的错误,并采取某种操作,如把错误记录在日志中,再回滚事务;

6.SELECT @@TRANCOUNT可用在代码的任何位置来判断当前使用SELECT @@TRANCOUNT的地方是否位于一个打开的事务当中,如果不在任何打开的事务范围内,则该函数返回0;如果在某个打开的事务返回范围内,则返回一个大于0的值。打开一个事务,@@TRANCOUNT=@@TRANCOUNT+1;提交一个事务,@@TRANCOUNT-1。

 

(2)一致性Consiitency

1.同时发生的事务在修改和查询数据时不发生冲突;

2.一致性取决于应用程序的需要。后面会讲到一致性级别,以及如何对一致性进行控制。

 

(3)隔离性Isolation

1.用于控制数据访问,确保事务只访问处于期望的一致性级别下的数据;

2.使用锁对各个事务之间正在修改和查询的数据进行隔离。

 

(4)持久性Durability

1.在将数据修改写入到磁盘上数据库的数据分区之前会把这些修改写入到磁盘上数据库的事务日志中,把提交指令记录到磁盘的事务日志中以后,及时数据修改还没有应用到磁盘的数据分区,也可以认为事务时持久化的。

2.系统重新启动(正常启动或在发生系统故障之后启动),SQL Server会每个数据库的事务日志,进行回复处理。

3.恢复处理包含两个阶段:重做阶段和撤销阶段。

4.前滚:在重做阶段,对于提交指令已经写入到日志的事务,但数据修改还没有应用到数据分区的事务,数据库引擎会重做这些食物所做的所有修改。

5.回滚:在撤销阶段,对于提交指令没有写入到日志中的事务,数据库引擎会撤销这些事务所做的修改。(这句话需要research,可能是不正确的。因为提交指令没有写入到数据分区,撤销修改是指撤销哪些修改呢???)

 

二、锁

1.事务中的锁

(1)SQL Server使用锁来实现事务的隔离。

(2)事务获取锁这种控制资源,用于保护数据资源,防止其他事务对数据进行冲突的或不兼容的访问。

2.锁模式

(1)排他锁

a.当试图修改数据时,事务只能为所依赖的数据资源请求排他锁。

b.持有排他锁时间:一旦某个事务得到了排他锁,则这个事务将一直持有排他锁直到事务完成。

c.排他锁和其他任何类型的锁在多事务中不能在同一阶段作用于同一个资源。

如:当前事务获得了某个资源的排他锁,则其他事务不能获得该资源的任何其他类型的锁。其他事务获得了某个资源的任何其他类型的锁,则当前事务不能获得该资源的排他锁。

(2)共享锁

a.当试图读取数据时,事务默认会为所依赖的数据资源请求共享锁。

b.持有共享锁时间:从事务得到共享锁到读操作完成。

c.多个事务可以在同一阶段用共享锁作用于同一数据资源。

d.在读取数据时,可以对如何处理锁定进行控制。后面隔离级别会讲到如何对锁定进行控制。

3.排他锁和共享锁的兼容性

(1)如果数据正在由一个事务进行修改,则其他事务既不能修改该数据,也不能读取(至少默认不能)该数据,直到第一个事务完成。

(2)如果数据正在由一个事务读取,则其他事务不能修改该数据(至少默认不能)。

4.可锁定的资源的类型

RID、KEY(行)、PAGE(页)、对象(例如表)、数据库、EXTENT(区)、分配单元(ALLOCATION_UNIT)、堆(HEAP)、以及B树(B-tree)。

RID: 标识页上的特定行
格式: fileid: pagenumber: rid (1:109:0 )
其中fileid标识包含页的文件, pagenumber标识包含行的页,rid标识页上的特定行。
fileid与sys.databases_files 目录视图中的file_id列相匹配
例子:
在查询视图sys.dm_tran_locks的时候有一行的resource_description列显示RID 是1:109:0 而status列显示wait,
表示第1个数据文件上的第109页上的第0行上的锁资源。

5.锁升级

SQL Server可以先获得细粒度的锁(例如行或页),在某些情况下将细粒度锁升级为更粗粒度的锁(例如,表)。
例如单个语句获得至少5000个锁,就会触发锁升级,如果由于锁冲突而导致无法升级锁,则SQL Server每当获取1250个新锁时出发锁升级。

三、阻塞

1.阻塞

当多个事务都需要对某一资源进行锁定时,默认情况下会发生阻塞。被阻塞的请求会一直等待,直到原来的事务释放相关的锁。锁定超时期限可以限制,这样就可以限制被阻塞的请求在超时之前要等待的时间。

阶段1:事务A请求资源S1,事务不对资源S1进行操作

阶段2:事务A用锁A锁定资源S1,事务B请求对资源S1进行不兼容的锁定(锁B),锁B的请求被阻塞,事务B将进入等待状态

阶段3:事务A正在释放锁A,事务B等待锁A释放,

阶段4:事务A的锁A已释放,事务B用锁B锁定资源S1

 

2.排除阻塞

例子:

(1)准备工作:

1.准备测试数据

--先创建一张表Product作为测试。id为表的主键,price为product的价格
CREATE TABLE [dbo].[myProduct](
	[id] [int] NOT NULL,
	[price] [money] NOT NULL
) ON [PRIMARY]
GO
--插入一条数据,id=1,price=10
INSERT INTO [TSQLFundamentals2008].[dbo].[myProduct]([id],[price])VALUES(1,10)

2.模拟阻塞发生的情况

在SQL Server中打开三个查询窗口Connection1、Connection2、Connection3,分别按顺序执行表格中的执行语句。

--Connection1
BEGIN TRAN
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1
 
--Connection2
SELECT * FROM dbo.myProduct WHERE id=1
 
--Connection3
SELECT  request_session_id AS 会话id ,
		resource_type AS 请求锁定的资源类型 ,
		resource_description AS 描述 ,
		request_mode AS 模式 ,
		request_status AS 状态
FROM    sys.dm_tran_locks
查询窗口 服务器进程标识符SPID 执行语句 结果 说明
 Connection1 52
--语句1:
BEGIN TRAN
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1

更新产品价格10.00->11.00

 

  为了更新id=1这一行数据,会话必须先获得一个排他锁。事务处于一直打开状态,没有提交,所以事务一直持有排他锁,直到事务提交并完成。
 Connection2 56
--语句2:
SELECT * FROM dbo.myProduct WHERE id=1

 

  事务为了读取数据,需要请求一个共享锁,但是这一行已经被其他会话持有的排他锁锁定,而且共享锁和排他锁不是兼容的,所以会话被阻塞,进入等待状态
 Connection3 57
--语句3:
SELECT request_session_id AS 会话id ,
resource_type AS 请求锁定的资源类型 ,
resource_description AS 描述 ,
request_mode AS 模式 ,
request_status AS 状态
FROM sys.dm_tran_locks

 

  会话56:
(1)状态WAIT-等待锁
(2)正在等待第1个数据文件上的第109页上的第0行资源的共享锁
(3)持有第1个数据文件上的第109页资源的意向共享锁
(3)持有OBJECT资源,意向共享锁
(4)持有DATABASE资源,意向共享锁
会话52:
(1)状态WAIT-授予锁
(2)正在等待第1个数据文件上的第109页上的第0行资源的排他锁(3)持有第1个数据文件上的第109页资源的排他锁
(3)持有OBJECT资源,排他锁
(4)持有DATABASE资源,排他锁

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(2)分析阻塞

★ 1.sys.dm_tran_locks 视图

(1)该动态视图可以查询出哪些资源被哪个进程ID锁了

(2)查询出对资源授予或正在等待的锁模式

(3)查询出被锁定资源的类型

上面的查询语句3已经用到了这个视图,可以参考上图中的分析说明。

 

★ 2.sys.dm_exec_connections 视图

(1)查询出该动态视图可以查询出进程相关的信息

(2)查询出最后一次发生读操作和写操作的时间last_read,last_write

(3)查询出进程执行的最后一个SQL批处理的二进制标记most_recent_sql_handle

查询窗口 服务器进程标识符SPID 执行语句 结果 说明
 Connection3 57
SELECT  session_id ,
        connect_time ,
        last_read ,
        last_write ,
        most_recent_sql_handle
FROM    sys.dm_exec_connections

WHERE   session_id IN ( 52, 56 )

 

 

 

会话52:
(1)connect_time连接时间:2016-06-07 07:09:41.103
(2)last_read最后一次读操作时间:2016-06-07 07:10:56.233
(3)last_write最后一次写操作时间:2016-06-07 07:10:57.873
(4)most_recent_sql_handle这是一个二进制标记,最后一个SQL批处理

会话56:
(1)状态WAIT-授予锁
(2)正在等待第1个数据文件上的第109页上的第0行资源的排他锁(3)持有第1个数据文件上的第109页资源的排他锁
(3)持有OBJECT资源,排他锁
(4)持有DATABASE资源,排他锁

 

 

 

 

 

 

 

 

 

 

★ 3.sys.dm_exec_sql_text 表函数

(1)该函数可以将二进制标记most_recent_sql_handle作为参数,然后返回SQL代码。

(2)阻塞进程在不断地运行,所以在代码中看到的最后一个操作不一定是导致问题的语句。在本例中最后一条执行语句是导致阻塞的语句。

查询窗口 服务器进程标识符SPID 执行语句 结果 说明
 Connection3  57
SELECT  session_id ,
        text
FROM    sys.dm_exec_connections
        CROSS APPLY sys.dm_exec_sql_text
        (most_recent_sql_handle) AS ST
WHERE   session_id IN ( 52, 56 )

 

会话52:
执行的SQL语句:

BEGIN TRAN
UPDATE dbo.myProduct
SET price = price + 1
WHERE id = 1

会话56:
执行的SQL语句:

(@1 tinyint)
SELECT * FROM [dbo].[myProduct] 
WHERE [id]=@1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

★ 4.sys.dm_exec_sessions 视图

(1)会话建立的时间login_time

(2)特定于会话的客户端工作站名称host_name

(3)初始化会话的客户端程序的名称program_name

(4)会话所使用的SQL Server登录名login_name

(5)最近一次会话请求的开始时间last_request_start_time

(6)最近一次会话请求的完成时间last_request_end_time

查询窗口 服务器进程标识符SPID 执行语句 结果 说明
 Connection3 57
SELECT * FROM sys.dm_exec_sessions
 

 

 

 

 

 

 

 

 

 

 

 

★ 5.sys.dm_exec_requests 视图

(1)识别出阻塞链涉及到的会话、争用的资源、被阻塞会话等待了多长时间

查询窗口 服务器进程标识符SPID 执行语句 结果 说明
 Connection3 57
SELECT * FROM sys.dm_exec_sessions
 

 

会话56:
(1)被会话52阻塞,blocking_session_id = 52
(2)会话52的开始时间start_time
(3)状态挂起status = suspended
(4)挂起的命令command=select

 

 

 

 

 

 

 

 

 

★ 6.Lock_TIMEOUT 选项

(1)设置会话等待锁释放的超时期限

(2)默认情况下会话不会设置等待锁释放的超时期限

(3)设置会话超时期限为5秒, SET Lock_TIMEOUT 5000

(4)锁定如果超时,不会引发事务回滚

(5)取消会话超时锁定的设置,SET LOCK_TIMEOUT -1

如果超时,将显示以下错误:

 

7.KILL <spid> 命令

(1)杀掉会话52,KILL 52

(2)杀掉会话,会引起事务回滚,同时释放排他锁

四、隔离级别

1.基本概念:

(1)隔离级别用来做什么

a.隔离级别用于决定如何控制并发用户读写数据的操作

(2)写操作
a.任何对表做出修改的语句

b.使用排他锁

c.不能修改读操作获得的锁和锁的持续时间

(3)读操作:

a.任何检索数据的语句

b.默认使用共享锁

c.使用隔离级别来控制读操作的处理方式

2.隔离级别的分类

(1)未提交读 (READ UNCOMMITTED)

(2)已提交读(READ COMMITTED)(默认值)

(3)可重复读(REPEATABLE READ)

(4)可序列化(SERIALIZABLE)

(5)快照(SNAPSHOT)

(6)已经提交读快照(READ_COMMITTED_SNAPSHOT)

3.隔离级别的设置

(1)设置整个会话的隔离级别

SET TRANSACTION ISOLATION LEVEL <isolation name>;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

(2)用表提示设置查询的隔离级别

SELECT ... FROM <table> WITH (<isolation name>);
SELECT * FROM dbo.myProduct WITH (READCOMMITTED);

注意:

1.设置会话选项的隔离级别时,隔离级别中的每个单词之间需要用空格分隔

2.用表提示的隔离级别时,隔离级别中的每个单词之间不需要用空格分隔

3.表提示的隔离级别有同义词,如:NOLOCK->READUNCOMMITTED,HOLDLOCK->REPEATABLEREAD

4.隔离级别的严格性:1.未提交读<2.已提交读<3.可重复读<4.可序列化

5.隔离级别越高,一致性越高,并发性越低

6.基于快照的隔离级别,SQL Server将提交过的行保存到tempdb数据库中,当读操作发现行的当前版本和它们预期的不一致时,可以立即得到行的以前版本,从而不用请求共享锁也能取得预期的一致性。

4.隔离级别的行为方式

★ 1.未提交读 (READ UNCOMMITTED)

打开两个查询窗口,Connetion1,connection2

Step1: 执行Connection1的阶段2的SQL 语句,然后执行connection2的SQL语句

Step2: 执行Connection1的阶段3的SQL 语句,执行connection2的SQL语句

Step3: 执行Connection1的阶段4的SQL 语句,执行connection2的SQL语句

查询窗口 事务  执行语句
Connetion1 A
--阶段2
UPDATE  myProduct
SET     price = price + 1
WHERE   id = 1;
 
SELECT  id ,
        price
FROM    dbo.myProduct
WHERE   id = 1;
 
--阶段3
UPDATE  myProduct
SET     price = price + 5
WHERE   id = 1;
 
SELECT  id ,
        price
FROM    dbo.myProduct
WHERE   id = 1;
 
--阶段4
COMMIT TRAN

 

Connection2 B
--在阶段2执行之后
SET TRAN ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRAN;
SELECT  id ,
        price
FROM    dbo.myProduct
WHERE   id = 1

COMMIT TRAN;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

两个事务的流程图:

 

 

阶段1:Price=10,事务A对myProduct表请求排他锁

阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1,然后事务A查询price的价格: price=11。事务B不请求任何锁,事务B在A更新Price之后进行查询,price=11

阶段3:事务A更新price = price + 5,然后事务A查询price的价格,price = 16。事务B查询price的价格: price=16

阶段4:事务A释放排他锁

阶段5:事务A中查询price的价格:price = 16。事务B查询price的价格: price=16

大家可以看到事务B有两种结果,这就是“未提交读 (READ UNCOMMITTED)”隔离级别的含义:

(1)读操作可以读取未提交的修改(也称为脏读)。

(2)读操作不会妨碍写操作请求排他锁,其他事务正在进行读操作时,写操作可以同时对这些数据进行修改。

(3)事务A进行了多次修改,事务B在不同阶段进行查询时可能会有不同的结果。

 

★ 2.已提交读(READ COMMITTED)(默认值)

打开两个查询窗口,Connetion1,connection2

Step1: 执行Connection1的SQL 语句

Step2: 执行Connection2的SQL 语句

执行语句 执行语句
Connetion1 A
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1
SELECT * FROM dbo.myProduct WHERE id =1

 

Connection2 B
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SELECT * FROM dbo.myProduct WHERE id = 1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

两个事务的流程图:

 

阶段1:Price=10,事务A对myProduct表请求排他锁

阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1,然后事务A查询price的价格: price=11。然后事务B请求共享锁进行读操作,查询price,

由于在当前隔离级别下,事务A的排他锁和事务B的共享锁存在冲突,所以事务B需要等待事务A释放排他锁后才能读取数据。

阶段3:事务A提交事务(COMMIT TRAN)

阶段4:事务A提交完事务后,释放排他锁

阶段5:事务B获得了共享锁,进行读操作,price=11

“已提交读 (READ UNCOMMITTED)”隔离级别的含义:

(1)必须获得共享锁才能进行读操作,其他事务如果对该资源持有排他锁,则共享锁必须等待排他锁释放。

(2)读操作不能读取未提交的修改,读操作读取到的数据是提交过的修改。

(3)读操作不会在事务持续期间内保留共享锁,其他事务可以在两个读操作之间更改数据资源,读操作因而可能每次得到不同的取值。这种现象称为“不可重复读”

 

★ 3.可重复读(REPEATABLE READ)

打开两个查询窗口,Connetion1,connection2

Step1: 执行Connection1的SQL 语句

Step2: 执行Connection2的SQL 语句

 

执行语句 事务 执行语句
Connetion1 A
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
SELECT * FROM dbo.myProduct WHERE id = 1
Connection2 B
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1

 

 

 

 

 

 

两个事务的流程图:

阶段1:Price=10,事务A对myProduct表请求共享锁

阶段2:事务A对myProduct表使用了共享锁,事务A查询price的价格: price=10,事务A一直持有共享锁直到事务A完成为止。然后事务B请求排他锁进行写操作price=price+1,

由于在当前隔离级别下,事务A的共享锁和事务B请求的排他锁存在冲突,所以事务B需要等待事务A释放共享锁后才能修改数据。

阶段3:事务A查询price, price=10, 说明事务B的更新操作被阻塞了,更新操作没有被执行。然后事务A提交事务(COMMIT TRAN)

阶段4:事务A提交完事务后,释放共享锁

阶段5:事务B获得了排他锁,进行写操作,price=11

“可重复读 (REPEATABLE READ)”隔离级别的含义:

(1)必须获得共享锁才能进行读操作,获得的共享锁将一直保持直到事务完成之止。

(2)在获得共享锁的事务完成之前,没有其他事务能够获得排他锁修改这一数据资源,这样可以保证实现可重复的读取。

(3)两个事务在第一次读操作之后都将保留它们获得的共享锁,所以任何一个事务都不能获得为了更新数据而需要的排他锁,这种情况将会导致死锁(deadlock),不过却避免了更新冲突。

★ 4.可序列化(SERIALIZABLE)

打开两个查询窗口,Connetion1,connection2
Step1: 执行Connection1的SQL 语句
Step2: 执行Connection2的SQL 语句

 

执行语句 事务
执行语句
Connetion1 A
BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
SELECT * FROM dbo.myProduct WHERE id = 1
Connection2 B
INSERT INTO dbo.myProduct(id, price) VALUES (1, 20)

 

 

 

 

 

 

 

两个事务的流程图:

 

阶段1:Price=10,事务A对myProduct表请求共享锁

阶段2:事务A对myProduct表使用了共享锁,事务A查询id=1的price的价格:1行记录,price=10,事务A一直持有共享锁直到事务A完成为止。然后事务B请求排他锁进行插入操作id=1,price=20,

由于在当前隔离级别下,事务B试图增加能够满足事务A的读操作的查询搜索条件的新行,所以事务A的共享锁和事务B请求的排他锁存在冲突,事务B需要等待事务A释放共享锁后才能插入数据。

阶段3:事务A查询出id=1的数据只有1行,说明事务B的插入操作被阻塞了,插入操作没有被执行。然后事务A提交事务(COMMIT TRAN)

阶段4:事务A提交完事务后,释放共享锁

阶段5:事务B获得了排他锁,进行插入操作,插入成功,查询出id=1的数据有两条

“可序列化(SERIALIZABLE)”隔离级别的含义:

(1)必须获得共享锁才能进行读操作,获得的共享锁将一直保持直到事务完成之止。

(2)在获得共享锁的事务完成之前,没有其他事务能够获得排他锁修改这一数据资源,且当其他事务增加能够满足当前事务的读操作的查询搜索条件的新行时,其他事务将会被阻塞,直到当前事务完成然后释放共享锁,其他事务才能获得排他锁进行插入操作。

(3)事务中的读操作在任何情况下读取到的数据是一致的,不会出现幻影行(幻读)。

(4)范围锁:读操作锁定满足查询搜索条件范围的锁

 

5.隔离级别总结

 

脏读:读取未提交的更改。

不可重复读:读操作不会在事务持续期间内保留共享锁,其他事务可以在两个读操作之间更改数据资源,读操作因而可能每次得到不同的取值。

丢失更新:两个事务进行读操作,获得资源上的共享锁,读取完数据后,不再持有资源上的任何锁,两个事务都能更新这个值,

最后进行更新的事务将会覆盖其他事务做的更改,导致其他事务更改的数据丢失。

幻读:第一次和第二次读取到的数据行数不一致。

范围锁:读操作锁定满足查询搜索条件范围的锁

 

隔离级别 是否读取未提交的行 是否不可重复读 是否丢失更新 是否幻读 共享锁持续时间 是否持有范围锁
未提交读 READ UNCOMMITTED Y Y Y Y 当前语句 N
已提交读 READ COMMITTED N Y Y Y 当前语句 N
可重复读REPEATABLE READ N N N Y 事务开始到事务完成 N
可序列化SERIALZABLE N N N N 事务开始到事务完成 Y

 

 

 

 

五.死锁

死锁是指一种进程之间互相永久阻塞的状态,可能涉及两个或更多的进程。

打开两个查询窗口,Connetion1,connection2

Step1: 执行Connection1的SQL 语句

Step2: 执行Connection2的SQL 语句

 

执行语句 事务 执行语句
Connetion1 A
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1
SELECT * FROM dbo.myOrder WHERE id =1
Connection2 B
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
UPDATE dbo.myOrder SET customer = 'ddd' WHERE id = 1
SELECT * FROM dbo.myProduct WHERE id = 1

 

 

 

 

 

 

 

两个事务的流程图:

阶段1:Price=10,事务A对myProduct表请求排他锁。Customer = aaa,事务B对myOrder请求排他锁

阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1。然后事务B对myOrder表使用了排他锁,更新customer=ddd。

阶段3:事务A查询myOrder表,对myOrder表请求共享锁,因为事务A的请求的共享锁与事务B的排他锁冲突,所以事务A被阻塞。然后事务B查询myProduct表,对myProduct表请求共享锁,因为事务B的请求的共享锁与事务A的排他锁冲突,所以事务B被阻塞。

阶段4:事务A等待事务B的排他锁释放,事务B等待事务A的排他锁释放,导致死锁。事务A和事务B都被阻塞了。

阶段5:SQL Server在几秒之内检测到死锁,会选择一个事务作为死锁的牺牲品,终止这个事务,并回滚这个事务所做的操作。在这个例子中,事务A被终止,提示信息:事务(进程 ID 53)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。

“死锁 (Dead Lock)”的一些注意事项:

(1)如果两个事务没有设置死锁优先级,且两个事务进行的工作量也差不多一样时,任何一个事务都有可能被终止。

(2)解除死锁要付出一定的系统开销,因为这个过程会涉及撤销已经执行过的处理。

(3)事务处理的时间时间越长,持有锁的时间就越长,死锁的可能性也就越大,应该尽可能保持事务简短,把逻辑上可以不属于同一个工作单元的操作移到事务以外。

(4)上面的例子中,事务A和事务B以相反顺序访问资源,所以发生了死锁。如果两个事务按同样的顺序来访问资源,则不会发生这种类型的死锁。在不改变程序的逻辑情况下,可以通过交换顺序来解决死锁的问题。

关于分析死锁的问题,可以参考前面写的关于阻塞的内容。

 

 

原文链接:30分钟全面解析-SQL事务+隔离级别+阻塞+死锁

参考资料:《T-SQL基础》