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

mikel阅读(1064)

来源: 常用的几个测试网络连接的命令_曲健磊的技术分享-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阅读(1879)

来源: 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阅读(532)

来源: 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阅读(436)

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

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

 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阅读(469)

来源: 谈谈分布式事务之三: 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阅读(477)

来源: 谈谈分布式事务之三: 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阅读(549)

来源: 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基础》

SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因 - BIWORK - 博客园

mikel阅读(491)

来源: SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因 – BIWORK – 博客园

原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫。所以花了点时间,把 SQL Server 数据库中的事务概念,ACID 原则,事务中常见的问题,问题造成的原因和事务隔离级别等这些方面的知识好好的整理了一下。

其实有关 SQL Server 中的事务,说实话因为内容太多, 话题太广,稍微力度控制不好就超过了我目前知识能力范围,就不是三言两语能够讲清楚的。所以希望大家能够指出其中总结的不足之处,对我来说多了提高的机会,更可以帮助大家加深对事务的理解。


本文涉及到的知识点:

  • SQL Server 数据库中事务的概念
  • ACID 原则 (加了一部分内容专门解释原子性,提到了显示事务以及 XACT_ABORT 机制来确保事务的原子性)
  • 列出事务中常见的问题以及原因:脏读,未提交读,不可重复读,幻读
  • SQL Server中 事务的隔离级别以及它们如何做到避免脏读,未提交读,不可重复读和幻读 (用代码描述了这些问题,并且使用时间序来解释产生的原因)

SQL Server 数据库中事务的概念

数据库中的事务是数据库并发控制的基本单位,一条或者一组语句要么全部成功,对数据库中的某些数据成功修改; 要么全部不成功,数据库中的数据还原到这些语句执行

之前的样子。比如网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量还是那么多。不允许出现你订票成功了,余票没有减少或者你取消订票了,余票显示却少了一张的这种情况。这种不被允许出现的情况就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。


数据库中事务的 ACID 原则

原子性 (Atomicity):事务的原子性是指一个事务中包含的一条语句或者多条语句构成了一个完整的逻辑单元,这个逻辑单元具有不可再分的原子性。这个逻辑单元要么一起提交执行全部成功,要么一起提交执行全部失败。

一致性 (Consistency):可以理解为数据的完整性,事务的提交要确保在数据库上的操作没有破坏数据的完整性,比如说不要违背一些约束的数据插入或者修改行为。一旦破坏了数据的完整性,SQL Server 会回滚这个事务来确保数据库中的数据是一致的。

隔离性(Isolation):与数据库中的事务隔离级别以及锁相关,多个用户可以对同一数据并发访问而又不破坏数据的正确性和完整性。但是,并行事务的修改必须与其它并行事务的修改相互独立,隔离。 但是在不同的隔离级别下,事务的读取操作可能得到的结果是不同的。

持久性(Durability):数据持久化,事务一旦对数据的操作完成并提交后,数据修改就已经完成,即使服务重启这些数据也不会改变。相反,如果在事务的执行过程中,系统服务崩溃或者重启,那么事务所有的操作就会被回滚,即回到事务操作之前的状态。

我理解在极端断电或者系统崩溃的情况下,一个发生在事务未提交之前,数据库应该记录了这个事务的”ID”和部分已经在数据库上更新的数据。供电恢复数据库重新启动之后,这时完成全部撤销和回滚操作。如果在事务提交之后的断电,有可能更改的结果没有正常写入磁盘持久化,但是有可能丢失的数据会通过事务日志自动恢复并重新生成以写入磁盘完成持久化。

原子性的进一步理解

关于原子性,有必要在这里多补充一下,因为我们描述的概念是指在事务中的原子性。一条 SQL 语句和多条 SQL 语句在处理原子性上是有一些区别的,下面演示了这些区别。

先运行这些代码,创建一个非常简单的测试表,这张表只简单模拟了一个账户的 ID 和账户余额。

USE BIWORK_SSIS
GO

IF OBJECT_ID('dbo.Account') IS NOT NULL
DROP TABLE dbo.Account
GO

CREATE TABLE dbo.Account
(
  ID INT PRIMARY KEY,
  AccountBalance MONEY CHECK(AccountBalance >= 0)
)

单条 SQL 语句的原子性

插入一条测试语句,然后再查询一下结果。

这里提到了自动提交事务,这时 T-SQL 默认的事务方式,它是一种能够自动执行并能够自动回滚事务的处理方式。SQL Server 除了自动提交事务之外,还有显示事务和隐式事务,暂时不在这篇文章中讨论它们的区别了。

上面的两个自动提交事务中,每一个自动提交事务只包含一条 SQL 语句,不能再分,要么成功,要么失败。

再比如,在一条 SQL 语句中插入多条数据时,其中一条数据是符合约束的。但因为另外一条数据违反了检查约束,这样也会导致整个 Insert 语句失败,因此没有一条数据能够插入到数据表中。

多条 SQL 语句形成的一个整体的原子性

假设下面的这两条 Insert 语句构成一个具备原子性特征的逻辑单元,是一个整体需要形成一个事务,那么应该如何处理。

INSERT INTO dbo.Account VALUES(1004,-1)
INSERT INTO dbo.Account VALUES(1005,500)

很显然如果直接这么执行的话,1004 插入失败,1005 可以插入成功,这样就是两个不同的事务了。SQL Server 提供了两种方式来确保这种包含多组 SQL 语句的逻辑块具备原子性特征。

方式一 – 使用显示事务组合多条 SQL 语句构成一个整体以实现事务的原子性

第一种就是非常常见的显示事务,通过显示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 命令将一组 SQL 语句形成一个完整的事务来提交,提交要么成功,要么失败。

-- 开始一个事务
BEGIN TRANSACTION

-- TRY CATCH 语句
BEGIN TRY

 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功,但此时事务还未真正提交
    INSERT INTO dbo.Account VALUES(1005,500)

END TRY
BEGIN CATCH
 -- 发生错误,事务回滚
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;

-- 没有进入 CATCH 块,提交事务
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION;
GO

当然最终的结果就是事务回滚,一条数据都没有插入到数据表中,所以失败时就全部失败,确保了事务的原子性。

方式二 – 通过设置  XACT_ABORT 为 ON 来确保事务的原子性

先来看默认的设置,当  XACT_ABORT 为 OFF 状态的时候。

-- SET XACT_ABORT OFF - 默认的 SQL Server 设置
SET XACT_ABORT OFF
BEGIN TRANSACTION
 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功
 INSERT INTO dbo.Account VALUES(1005,500)
COMMIT TRANSACTION

当  XACT_ABORT 为 OFF 状态即 SQL Server 默认设置下,上面的事务中,SQL Server 在通常情况下只会回滚执行失败的语句,也就是说只会回滚 1004 这条数据,而 1005 会插入成功。很显然,这违背了事务的原子性,因为我们也没有显示的写出要 ROLLBACK TRANSACTION 来。

OK!那我们将 XACT_ABORT 设置为 ON,这时就告诉了它后面的事务,如果遇到错误就立即终止事务并回滚。这样不通过显示的 ROLLBACK TRANSACTION 也可以确保事务的原子性。

在上面的这个例子中,只有事务 2 会成功提交,而事务1和3会回滚,插入操作执行失败。

注意一点,上面的每个事务后面加了一个 GO 关键字,如果不加 GO 这个关键字,一起执行这些 SQL 语句会导致事务2和3因为事务1的执行失败而不能执行到, GO 关键字形成了一个批处理,表示前面的一组 SQL 语句一起处理。

GO 关键字非常有意思,GO 后面可以加上次数,表示前面的一条或者一组 SQL 执行几次。

通过上面的示例,应该可以理解原子性与事务的关系了,以及如何实现事务的原子性。


事务中常见的问题

了解完事务的 ACID 的原则后,再来看看在 SQL Server 中多用户并发的情况下,使用事务可能会遇到的一些情况:

脏读 (Dirty Reads) : 一个事务正在访问并修改数据库中的数据但是没有提交,但是另外一个事务可能读取到这些已作出修改但未提交的数据。这样可能导致的结果就是所有的操作都有可能回滚,比如第一个事务对数据做出的修改可能违背了数据表的某些约束,破坏了完整性,但是恰巧第二个事务却读取到了这些不正确的数据造成它自身操作也发生失败回滚。

不可重复读取(Non-Repeatable Reads):  A 事务两次读取同一数据,B事务也读取这同一数据,但是 A 事务在第二次读取前B事务已经更新了这一数据。所以对于A事务来说,它第一次和第二次读取到的这一数据可能就不一致了。

幻读(Phantom Reads): 与不可重复读有点类似,都是两次读取,不同的是 A 事务第一次操作的比如说是全表的数据,此时 B 事务并不是只修改某一具体数据而是插入了一条新数据,而后 A 事务第二次读取这全表的时候就发现比上一次多了一条数据,发生幻觉了。

更新丢失(Lost Update): 两个事务同时更新,但由于某一个事务更新失败发生回滚操作,这样有可能的结果就是第二个事务已更新的数据因为第一个事务发生回滚而导致数据最终没有发生更新,因此两个事务的更新都失败了。


SQL Server 中事务的隔离级别以及与脏读,不可重复读,幻读等关系(代码论证和时间序)

了解了在并发访问数据库的情况下可能会出现这些问题,就可以继续了解数据库隔离级别这样的一个概念,通俗一点讲就是:你希望通过何种方式让并发的事务隔离开来,隔离到什么程度?比如可以容忍脏读,或者不希望并发的事务出现脏读的情况,那么这些可以通过隔离级别的设置使得并发事务之间的隔离程度变得宽松或者很严峻。

隔离级别越高,读取脏数据或者造成数据不统一不完整的机会就越少,但是在高并发的系统中,性能降低就越严重。隔离级别越低,并发系统中性能上提升很大,但是数据本身可能不完整。

在 SQL Server 2012 中可以通过这样的语法来设置事务的隔离级别 (从低到高排列):

SET TRANSACTION ISOLATION LEVEL
    { READ UNCOMMITTED
    | READ COMMITTED
    | REPEATABLE READ
    | SNAPSHOT
    | SERIALIZABLE
    }
[ ; ]

下面通过代码示例来演示各个事务隔离级别的表现,运行下面 SQL 语句,插入一条测试语句。

TRUNCATE TABLE BIWORK_SSIS.dbo.Account
GO

INSERT INTO BIWORK_SSIS.dbo.Account VALUES(1001,1000)

SELECT * FROM BIWORK_SSIS.dbo.Account
GO

Read Uncommitted (未提交读)

隔离级别最低,容易产生的问题就是脏读,因为可以读取其它事务修改了的但是没有提交的数据。它的作用跟在事务中 SELECT 语句对象表上设置 (NOLOCK) 相同。

打开两个查询窗口,第一个窗口表示事务 A, 第二个窗口表示事务B。 事务A 保持默认的隔离级别,事务B 设置它们的隔离级别为 READ UNCOMMITTED, 可以通过 DBCC USEROPITIONS 查看更改后的结果。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DBCC USEROPTIONS

测试步骤:

先执行事务 A 的 SQL 代码

BEGIN TRANSACTION

UPDATE BIWORK_SSIS.dbo.Account
SET AccountBalance = 500 
WHERE ID  = 1001

WAITFOR DELAY '00:00:10'

ROLLBACK TRANSACTION

SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

马上接着再执行 事务 B 的 SQL 代码

-- 第1次查询 发生在 A 事务未提交或者回滚之前
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

WAITFOR DELAY '00:00:10'

-- 第2次查询 发生在 A 事务回滚之后
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

可以看出,事务 B 对 ID = 1001 的这条数据进行了两次读取,但是很显然第一次读取的数据是脏数据。下面模拟了一下它们发生的时序,虽然不算严谨,但是可以帮助理解脏读产生的原因。

还可以把事务B 的隔离级别改回来成为默认的  READ COMMITTED,然后运行完事务 A 之后马上运行带有 NOLOCK 的查询,效果和上面描述的也是一致的。 一旦加上 NOLOCK,可以认为它的作用就等同于隔离级别为 READ UNCOMMITTED。

SELECT * FROM BIWORK_SSIS.dbo.Account WITH(NOLOCK) WHERE ID = 1001

 

Read Committed (已提交读)

这是 SQL Server 的默认设置,已提交读,可以避免脏读,可以满足大多数要求。事务中的语句不能读取已由其它事务做出修改但是还未提交的数据,但是能够读取由其它事务做出修改并提交了的数据。也就是说,有可能会出现 Non-Repeatable Reads 不可重复读取和 Phantom Reads 幻读的情况,因为当前事务中可能出现两次读取同一资源,但是两次读取的过程之间,另外一事务可能对这一资源完成了读取更新并提交的行为,这样数据前后可能就不一致了。因此,这一个默认的隔离级别能够解决脏读但是解决不了 Non-Repeatable Reads 不可重复读。

接着上一个例子,看看如果将隔离级别设置为 READ COMMITTED,能否避免脏读? 还是先运行事务 A,再接着运行事务 B。

因为已提交读不能读取已由其它事物做出修改但是还未提交的数据,因此事务B 就必须等待事务 A 完成对数据的修改提交或者回滚之后才能开始读取。运行事务A 和事务B,明显事务B 有一个等待事务A提交或者回滚的过程,看看它们的时序图。

由此可以看出隔离级别 READ COMMITTED 可以避免脏读,但是也有可能出现其它的问题,请看这个例子。先执行事务A,接着直接执行事务 B。

从上面的执行结果来看,很明显在事务 A 中,同一个事务中对 ID  = 1001 的取值出现了前后不一致的情况。假设这里不是简单的查询,而是先查询账户余额有 1000元钱,然后后面的动作就是取 1000元钱,很明显第二次取的时候发现只有 500 元了。原因就是在第一次查询和取的间隙之间被事务 B 钻了空子,修改了余额。这种情况就是上面所介绍到的不可重复读取,请看下面的时序图。

所以 READ COMMITTED 已提交读隔离级别能够避免脏读,但是仍然会遇到不可重复读取的问题。

Repeatable Read (可重复读)

不能读取已由其它事务修改了但是未提交的行,其它任何事务也不能修改在当前事务完成之前由当前事务读取的数据。但是对于其它事务插入的新行数据,当前事务第二次访问表行时会检索这一新行。因此,这一个隔离级别的设置解决了 Non-Repeatable Reads 不可重复读取的问题,但是避免不了 Phantom Reads 幻读。

接着上面的例子做出一些修改,增加了一些查询,记得把 ID = 1001 的余额改回 1000。将事务 A 的隔离级别设置为 REPEATABLE READ 可重复读级别,来看看这个隔离级别的表现。

尽管在最后的查询结果中, ID  = 1001 的余额为 500 元,但是在事务 A 中的两次读取一次发生在 事务 B 开始之前,一次发生在 事务 B 提交之后,但是它们读取的余额是保持一致的,看不到事务 B 对这个值的修改。

从上面的时序图中可以看出,事务 A 第一次读取到的 ID = 1001 的余额值和第二次读取到的是一样的,可以理解为在事务 A 的查询期间是不允许事务 B 修改这个值的。 因为事务 A 确实没有看到这个变化,所以事务A 也确实认为事务B 听了它的话,没有做出 Update 的操作。但是实际上,事务 B 已经完成了这个操作,只不过由于 事务 A 中隔离级别设置为 REPEATABLE READ 可重复读,所以两次读取的结果始终保持着一致。

那么这里的示例是事务B在修改数据,如果是新增加一行记录呢?

事务 A 又开始晕菜了!居然两次查询的结果不一样,第二次查询多了一条数据,这就是幻读!

SNAPSHOT (快照隔离)

可以解决幻读 Phantom Reads 的问题,当前事务中读取的数据在整个事务开始到事务提交结束之间,这个数据版本是一致的。其它的事务可能对这些数据做出修改,但是对于当前事务来说它是看不到这些变化。有点类似于当前事务拿到这个数据的时候是拿到这个数据的快照,因此在这个快照上做出的操作同一事务中前后几次操作都是基于同一数据版本。因此,这一个隔离级别的设置可以解决 Phantom Reads 幻读问题。但是要注意的是,其它事务是可以在当前事务完成之前修改由当前事务读取的数据。

在使用 SNAPSHOT 之前要注意,默认情况下数据库不允许设置 SNAPSHOT 隔离级别,直接设置会出现类似于这样的错误:

DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Msg 3952, Level 16, State 1, Line 8

Snapshot isolation transaction failed accessing database ‘BIWORK_SSIS’ because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.

所以要使用 SET 命令开启这个支持

ALTER DATABASE BIWORK_SSIS
SET ALLOW_SNAPSHOT_ISOLATION ON

并且在开始前先清空其它的 ID,只保留 ID = 1001 的这条记录。

DELETE FROM BIWORK_SSIS.dbo.Account
WHERE ID <> 1001

这样通过设置隔离级别是 SNAPSHOT就解决了幻读的问题,保证了在事务 A 中查询的数据行版本是前后一致的。

但是大家发现没有?无论在事务 A 中使用 Repeatable Read 还是 Snapshot 仍然不可避免的阻止事务B 对共享的资源做出了修改,尽管这个修改没有被事务 A 发现,事务 A 中的数据还是保持了一致,但是实际上还是做出了修改。只要事务 A 一提交结束,马上就可以看到事务 B 做出的这些修改已经生效了。回顾之前提到的,如果我第一次查询有1000元,第二次动作可能就是取1000元。在这两次动作之间另外的一个事务对金额做出了修改,尽管我两次读取都是1000元,但是实际上是不符合常理的。要么,我先查询然后再取款这个动作是连贯的,然后另外一个事务再对金额做出修改。要么,其它事务先对金额做出修改,比如扣去500元,那么我再查询再取款这个钱数还是一致的。也就是说,在事务 A 对某一个资源做出操作的时候,形成了独占,事务 B 进不来。或者事务 B 在对这个资源做操作的时候,事务 A 也必须等待事务 B 结束后才能开始它的事务,那么这里就要使用到最严格的隔离级别了 – SERIALIZABLE。

 

SERIALIZABLE(序列化)

性能最低,隔离级别最高最严格,可以几乎上面提到的所有问题。比如不能读取其它已由其它事务修改但是没有提交的数据,不允许其它事务在当前事务完成修改之前修改由当前事务读取的数据,不允许其它事务在当前事务完成修改之前插入新的行。它的作用与在事务内所有 SELECT 语句中的所有表上设置 HOLDLOCK 相同,并发级别比较低但又对安全性要求比较高的时候可以考虑使用。如果并发级别很高,使用这个隔离级别,性能瓶颈将非常严重。

将事务 A 的隔离级别调整成 SERIALIZABLE,然后执行 A 然后再执行 B。

在这里可以看到事务B 的执行基本上是在事务A提交之后才开始的,当事务 A 在执行的时候,事务 B 因为也要访问这个资源所以一直阻塞在那里直到事务 A 提交。 并不是说事务 B 没有开始,而是说在执行 SELECT 查询的时候因为事务 A 占用了这个资源,所以处于等待状态。

在 SQL Server 中设置隔离级别要注意:一次只能设置一个隔离级别的选项,并且设置的隔离级别对当前连接一直有效直到显式修改为止。事务中执行的所有读取操作也都会在指定的隔离级别规则下运行,除非在 SELECT 操作语句中对表指定了其它的锁或者版本控制行为。

注:上面的时序图只是用来帮助理解事务的隔离级别,只是一个大概的执行顺序,当然也跟我执行事务 A 和 事务 B 的时间点相关,所以并不能真正反映实际过程中 SQL 语句提交和执行的实际顺序,真正提交的过程可以通过 SQL Profiler 去跟踪看看。

解决sql 2008 事务日志收缩与截断无法减小日志物理文件的方法_zhen520的专栏-CSDN博客

mikel阅读(630)

来源: (7条消息)解决sql 2008 事务日志收缩与截断无法减小日志物理文件的方法_zhen520的专栏-CSDN博客

本文的重点是与大家分享日志截断、收缩后,物理文件的尺寸依然没有减小到期望尺寸的问题。

解决方案

   我的一个数据库,数据文件10+G ,事务日志达20+G,而且使用常规的截断、收缩方法均无法减小日志物理文件的尺寸,经过一番寻找,终于找到了解决方法。 查看日志信息 在查询分析器中执行如下代码来查看日志信息: 1 DBCC LOGINFO(‘数据库名称’) 我们看到status=0的日志,代表已经备份到磁盘的日志文件;而status=2的日志还没有备份。当我们收缩日志文件时,收缩掉的空间其实就是status=0的空间,如果日志物理文件无法减小,这里一定能看到非常多status=2的记录。接下来分析为什么会有这么多status=2的记录 查看日志截断延迟原因 活跃(active)的日志无法通过收缩来截断,有各种原因会使日志截断延迟,具体表现就是事务日志的物理文件无法通过截断、收缩来减小,通过下面的代码可以看到实例上每个数据库的日志截断延迟原因: 1 USE [master] 2 SELECT [name] ,[database_id] ,[log_reuse_wait] ,[log_reuse_wait_desc] FROM [sys].[databases] 各种原因及解释如下: log_reuse_wait_desc 值 说明 NOTHING 当前有一个或多个可重复使用的虚拟日志文件。 CHECKPOINT 自上次日志截断之后,尚未出现检查点,或者日志头部尚未跨一个虚拟日志文件移动(所有恢复模式)。 这是日志截断延迟的常见原因。有关详细信息,请参阅检查点和日志的活动部分。 LOG_BACKUP 需要日志备份,以将日志的头部前移(仅适用于完整恢复模式或大容量日志恢复模式)。 注意:日志备份不会妨碍截断。 完成日志备份后,日志的头部将前移,一些日志空间可能变为可重复使用。 ACTIVE_BACKUP_OR_RESTORE 数据备份或还原正在进行(所有恢复模式)。 数据备份与活动事务的运行方式相同。数据备份在运行时,将阻止截断。有关详细信息,请参阅本主题后面的“数据备份操作与还原操作”部分。 ACTIVE_TRANSACTION 事务处于活动状态(所有恢复模式)。 一个长时间运行的事务可能存在于日志备份的开头。在这种情况下,可能需要进行另一个日志备份才能释放空间。有关详细信息,请参阅本主题后面的“长时间运行的活动事务”部分。 事务被延迟(仅适用于 SQL Server 2005 Enterprise Edition 及更高版本)。“延迟的事务 ”是有效的活动事务,因为某些资源不可用,其回滚受阻。有关导致事务延迟的原因以及如何使它们摆脱延迟状态的信息,请参阅延迟的事务。 DATABASE_MIRRORING 数据库镜像暂停,或者在高性能模式下,镜像数据库明显滞后于主体数据库(仅限于完整恢复模式)。 有关详细信息,请参阅本主题后面的“数据库镜像与事务日志”部分。 REPLICATION 在事务复制过程中,与发布相关的事务仍未传递到分发数据库(仅限于完整恢复模式)。 有关详细信息,请参阅本主题后面的“事务复制与事务日志”部分。 DATABASE_SNAPSHOT_CREATION 正在创建数据库快照(所有恢复模式)。 这是日志截断延迟的常见原因,通常也是主要原因。 LOG_SCAN 正在进行日志扫描(所有恢复模式)。 这是日志截断延迟的常见原因,通常也是主要原因。 针对延迟日志截断原因的部分解决方案 LOG_BACKUP 备份日志后再执行收缩即可 REPLICATION 这是我遇到的情况,但我根本没有启用过REPLICATION,据查,这好像是SQLServer2008的一个BUG,解决方法是给标有“REPLICATION”的数据库任意一个表创建数据库事务复制(TRANSACTION REPLICATION),然后再删除,执行数据库与日志备份后,就可以收缩了。 小技巧一般收缩日志的代码中都要求指定日志的文件名称,下面的代码则可以自动获取日志文件名称: 1 USE [数据库名称] 2 DECLARE @LogFileLogicalName sysname 3 SELECT @LogFileLogicalName=Name FROM sys.database_files WHERE Type=1 4 PRINT @LogFileLogicalName 5 DBCC SHRINKFILE (@LogFileLogicalName, 1);

SQL Server 数据库开启日志CDC记录,导致SQL Server 数据库日志异常增大 - 蓝涩街灯 - 博客园

mikel阅读(902)

来源: SQL Server 数据库开启日志CDC记录,导致SQL Server 数据库日志异常增大 – 蓝涩街灯 – 博客园

这几天单位的SQL Server业务数据生产库出现数据库日志增长迅速,导致最终数据无法写入数据库,业务系统提示“数据库事务日志已满”,经过多方咨询和请教,终于将日志异常的数据库处理完毕,现总结下处理过程,希望可以帮助到同样遇到此问题的小伙伴。–在菜鸟的路上越走越远

–第一步:查询服务器上所有日志大小情况
DBCC SQLPERF(LOGSPACE)

–第二步:查询日志设置方式
SELECT name,recovery_m odel_desc,log_reuse_wait,log_reuse_wait_desc
FROM sys.databases where log_reuse_wait=6

–第三步:定位数据库
USE BAK
GO

–第四步:查看是否开启cdc功能
select is_tracked_by_cdc,*from sys.tables where is_tracked_by_cdc=1

–第五步:清理到活动日志,把日志标记为已经发布状态
EXEC sp_repldone @xactid = NULL, @xact_segno = NULL, @numtrans = 0, @time = 0, @reset = 1
–第六步:
EXEC sys.sp_cdc_disable_db
GO

–第七步:
EXEC sp_removedbreplication BAK
GO

–第八步:手动收缩数据库日志,详细收缩方式https://www.cnblogs.com/since-1995/p/11694373.html