抖音抓包 - maanshancss - 博客园

mikel阅读(1160)

来源: 抖音抓包 – maanshancss – 博客园

抓取某一个人的所有视频

这个网络上已经有了,我测试一下是OK的,都可以正常下载;你可以过滤一些自己不想看的视频

抓取自己点赞过的视频

因为我点赞过的视频过一段时间后作者会把它删除掉,所以我想把它下载下来,又不想一个个的点击下载;

我在网上找了很久都没有找到自己所需要的东西,都是一些乱七八糟的东西,别告诉我你直接用抖音官网API可以;
想看经历的看上一篇博客: 手机App的 Https协议抓包历程

Web代理 Charles

看我另外一篇博客 :Charels 抓包工具

手机端

  • 安装软件: VirtualXpose(核心) + JustTrustMe +抖音app
  • 配置网络代理为 192.168.3.9:8888 这个是我自己电脑的IP地址

得到json文件结果

  • 手动滑动手机或者安装按键助手 拖动 我->喜欢 那个列表得到
    比如安装Auto.js脚本如下:
    “auto”;
    swipe(500,1400,600,1200);
    另外设置循环次数为500,时间间隔为0.1s,延迟50s(根据你打开抖音app所需要的时间),过一段时间后自己看着是否已经滑动到最下面了;

选中所有右键SaveAll 得到所有列表的json,把这些文件保存到C盘的新建文件夹里面(路径我写死了);

下载视频 C

  • 读取json文件
    从 static string m_strJSONFolder = @”C:\新建文件夹”; 读取所有json文件,文件的内容如下:
  • 解析视频的名称
  • 解析视频的下载路径
    源码地址: 下载链接

下载视频

下载结果类似于下面,作者名称+作品名称+[AwemeId] 当作品名称相同的时候用作品ID
特别注意地址过一段时间后会失效,所以拷贝到后就立刻执行下载

收藏
关注
评论
作者:王思明
出处:http://www.cnblogs.com/maanshancss/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Python爬虫---爬取抖音短视频 - merlin& - 博客园

mikel阅读(1084)

来源: Python爬虫—爬取抖音短视频 – merlin& – 博客园

前言

最近一直想要写一个抖音爬虫来批量下载抖音的短视频,但是经过几天的摸索我发现了一个很严重的问题……抖音实在是难爬!从一开始的网页分析中就有着很多的坑,但是这几天的摸索也不是一无所获,我鼓捣出来了一个问题版的抖音爬虫(操作较为复杂),所以我也想通过这篇博客来记录下我分析网页的过程,也想请教一下路过大佬们,欢迎各位大佬指出问题!(这篇文章的分析比较麻烦了,但是也可以当做分析其他网页的一个参考吧,想要爬抖音的朋友们可以看这一篇的代码改良版抖音爬虫)

抖音爬虫制作

选定网页

想要爬取抖音上面的视频,就要先找到可以刷小视频的地址,于是我就开始在网上寻找网页版的抖音。经过一番寻找,发现抖音根本就没有网页版的这个板块,打开的网页大多都是如下图所示提示你下载app的网页:

想要爬取小视频的内容,没有网页地址可不行。于是我又想到了另一种寻找网页的方法:

首先我打开了手机抖音,选定了一个喜欢的抖音号,使用复制链接的方法来尝试是否可以在网页中打开:

将链接粘贴到记事本中,发现它是长这个样子的

https://v.douyin.com/wGf4e1/

将这个网址在浏览器中打开,发现这个网址可以正常显示

向下滑动,也可以看到这个账号发布的视频

ok,到现在为止,我已经选定了将这个页面作为我获取数据的起始页面

选定起始页之后,我的下一步想法是要去获取这些小视频的单独的网页地址,于是我又点击了下面的这些小视频。这里恶心的地方出现了,无论我点击哪一个小视频,弹出来的都是强迫你下载app的界面

于是我又想尝试上面获取到网页的操作来获取视频的地址,再次打开手机上的抖音,在这个漫威的账号下随便打开一个视频,点击右下角的分享,复制它的链接:

这个链接地址长这个样子:

#黑寡妇 北美终极预告!黑暗过往揭晓,2020即将强势开启漫威电影宇宙第四阶段! https://v.douyin.com/wGqCNG/ 复制此链接,打开【抖音短视频】,直接观看视频!

我再把这段地址复制到浏览器中打开:

打开的确实是视频页,点击播放按钮也可以播放视频,所以这就是我们需要记住的第二个页面。

分析网页

现在又出现了一个比较麻烦的事情,在浏览器中输入网址过后,跳转到了视频的播放页,但是此时的播放页地址经过了重定向生成了非常长的一串地址,乍一看毫无规律可讲

这是重定向之后的网址:

正常来说请求第一种链接https://v.douyin.com/wGqCNG/和第二种重定向之后的链接都可以获取到信息,但是我发现第一种链接地址是找不到规律的,所以我猜测第二种网址的规律会更加的好找,先把链接地址复制到记事本中:

https://www.iesdouyin.com/share/video/6802189485015633160/?region=CN&mid=6802184753988471559&u_code=388k48lba520&titleType=title&utm_source=copy_link&utm_campaign=client_share&utm_medium=Android &app=aweme

看了这么长一串的链接,链接包含的内容也是非常多的,对分析规律有着很大的干扰,于是我试着精简一下这个链接(删掉链接里面的一些内容,看看是否还能找到页面)经过了一次又一次的尝试,我所得到的最简单的网址如下:

https://www.iesdouyin.com/share/video/6802189485015633160/?mid=6802184753988471559

这个网址依旧可以打开视频页,如果在删掉一点东西,出来的就是抖音的宣传页,所以这个网址就是我所需要的最简单网址

就一个网址当然是分析不出规律的,于是我又用同样的方法来得到两个新网址:

https://www.iesdouyin.com/share/video/6818885848784702728/?region=CN&mid=6818885858780203783&u_code=388k48lba520&titleType=title&utm_source=copy_link&utm_campaign=client_share &utm_medium=Android&app=aweme https://www.iesdouyin.com/share/video/6820605884050181379/?region=CN&mid=6820605916115864328&u_code=388k48lba520&titleType=title&utm_source=copy_link&utm_campaign=client_share &utm_medium=Android&app=aweme

精简网址之后,将三个网址放在一起观察:

https://www.iesdouyin.com/share/video/6802189485015633160/?mid=6802184753988471559 https://www.iesdouyin.com/share/video/6818885848784702728/?mid=6818885858780203783 https://www.iesdouyin.com/share/video/6820605884050181379/?mid=6820605916115864328

不难发现,这三个网址的区别就在于数字的不同

接下来猜测:这串数字会不会是每个视频的Id值?

随后我打开漫威影业抖音号,右击检查,按下ctrl+f搜索内容,在搜索框内分别搜索https://www.iesdouyin.com/share/video/6802189485015633160/?mid=6802184753988471559链接中的68021894850156331606802189485015633160两个值

第一个值顺利找到,就是我们所猜测的id值,但是搜索第二个值却得不到任何的返回

这就叫我非常苦恼了,我开始想其他的办法来获取到这个值,我尝试了抓包和其他的一些方法,但是都没有找到这个值的相关信息。

我经过了一番思考,突然间冒出了一个想法:这个值是不是随机生成的?

然后我做了一个小小的尝试,我将这个值改成了随便的一个数

https://www.iesdouyin.com/share/video/6802189485015633160/?mid=18987

然而神奇的是,这个网址请求出了数据(去掉这个mid键不出数据,将mid随机赋值却可以得到数据emmm)

提取id构造网址

经过刚刚的分析,我发现我们想要提取的数据就只有一个id值,然后再用Id值替换掉网址中的数字就可以得到相应的视频页面了

id是这段代码的最重要的部分,经过前面曲折又困难的网页分析之后,我认为提取id只需要从网页中用表达式提取数据就可以了,但是我没想到的是这一步也是比较困难的

我先是在主页右击了检查,然后仔细的观察了elements里面的元素,我发现id就储存在elements之中

接着我就想办法获得这个页面中的id信息,我尝试了直接请求,发现输出的数据中没有Id信息; 我又加上了请求头,依旧没有id值输出(这个页面的元素是动态加载的,虽然不能一次获取全部Id,但是也不至于一个没有) ;然后我想到了selenium自动化测试模块,使用webdriver打开网址打印源码,可是输出还是没变

我查了百度的一些方法,也做了一些尝试,发现这个页面所做的反爬确实很难破解。于是我就只能换一条路去尝试

在谷歌开发者工具中,点击network选项卡,刷新界面 随着我刷新的操作,在XHR选项卡下出现了一个名字很奇怪的数据包

点击他的preview选项卡,点出他的下拉菜单,这里面存储的正是小视频的Id信息。但是需要注意的是,这个包里面只存储了0-20序列的共21条信息

但是从这个主页中我可以知道,它一共发布了64个短视频,所以我推断他还有三个数据包没有加载出来 —->我滚动下拉条,观察有没有数据包

滚动到最后,验证了我的猜想,这个页面有四个数据包且为动态加载

好了,我们已经分析出来了这个网页的构造,先不要管一共有几个数据包,先以一个数据包为例提取id信息:

复制网页链接作为他的请求网址

将user-agaent加到请求头中(这个网页必须要加请求头,不然获取不到数据)

请求得到数据:

import requests import json class Douyin: def page_num(self,max_cursor): #网址后面的随机参数(我实在分析不出规律) random_field = ‘RVb7WBAZG.rGG9zDDDoezEVW-0&dytk=a61cb3ce173fbfa0465051b2a6a9027e’ #网址的主体 url = ‘https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=MS4wLjABAAAAF5ZfVgdRbJ3OPGJPMFHnDp2sdJaemZo3Aw6piEtkdOA&count=21&max_cursor=0&aid=1128&_signature=’ + random_field #请求头 headers = { ‘user-agent’:‘Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36’, } response = requests.get(url,headers=headers).text #转换成json数据 resp = json.loads(response) #遍历 for data in resp[“aweme_list”]: # id值 video_id = data[‘aweme_id’] # 视频简介 video_title = data[‘desc’] # 构造视频网址 video_url = ‘https://www.iesdouyin.com/share/video/{}/?mid=1’ # 填充内容 video_douyin = video_url.format(video_id) print(video_id) print(video_title) print(video_douyin) if __name__ == ‘__main__’: douyin = Douyin() douyin.page_num()

print一下i相关信息:

当前乍一看好像这个代码没有问题,但是在执行四五次之后,会出现请求不到数据或返回False的情况,一开始我以为是ip被限制的原因,但是加上了ip池之后也是一样的结果,后来我才发现:请求不到数据是因为之前请求的url被禁用了,要在抖音详情页刷新一下,再把新的数据包的网址复制过来才能重新得到数据(我也不知道这是什么类型的反爬,希望知道的老哥可以告诉我一下)

*我之所以把这个网址分成两部分来写,是因为当网址请求不到数据的时候,改变的是末尾的_signature=random_field字段,在请求不到数据的时候只需重新复制一下这个字段就可以了,会简化一点点代码

拼接数据包链接

上面提取Id的时候讲到,我们先拿一个数据包做例子,但是我们要爬的这个用户的全部视频,所以就要将它所有的数据包地址都访问一遍

想要得到这些数据包的地址,就需要分析他们的网址构造,我把这四个数据包的网址全部复制到记事本中,逐个分析他们构造规律

不难看出,这四个数据包的区别就在于max_cursor后面的值的不同,而这个值正好就包包含在它前一个数据包之中,这就说明我们可以从前一个数据包中提取到下一个数据包的max_curso值,从而构造出下一个数据包的链接地址

可是第四个数据包也包含着max_cursor的值,我们该在何时停止构造下一个数据包呢?

我把最后一个数据包的max_cursor值复制下来并替换到构造的数据包链接中,发现可以跳转到一个新的网址,这个网址中也有max_cursor的值,但是这个值为0

也可以多找几个网址测试,最后的结果指向都是0,所以我们可以通过if语句来判断这个值为0的时候就终止循环
构造网址代码:

import requests import json class Douyin: def page_num(self,max_cursor): #网址后面的随机参数(我实在分析不出规律) random_field = ‘pN099BAV-oInkBpv2D3.M6TdPe&dytk=a61cb3ce173fbfa0465051b2a6a9027e’ #网址的主体 url = ‘https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=MS4wLjABAAAAF5ZfVgdRbJ3OPGJPMFHnDp2sdJaemZo3Aw6piEtkdOA&count=21&max_cursor=’ + str(max_cursor) + ‘&aid=1128&_signature=’ + random_field #请求头 headers = { ‘user-agent’:’Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36‘, } response = requests.get(url,headers=headers).text #转换成json数据 resp = json.loads(response) #提取到max_cursor max_cursor = resp[‘max_cursor’] #判断停止构造网址的条件 if max_cursor==0: return 1 else: print(url) douyin.page_num(max_cursor) if __name__ == ‘__main__’: douyin = Douyin() douyin.page_num(max_cursor=0)

输出构造后的网址:

获取视频地址

现在我们已经可以成功的进入获取到视频页面了,复杂的网页分析基本已经结束了,后续操作就变得简单了
先打开小视频的页面,右击检查,查看元素

经过检查发现,这个源代码中并没有视频地址,点击了一下播放按键,视频的地址才会加载出来

这个网址中只包含了一个小视频

看到这种动态加载的机制,我们首先就应该想到selenium自动化测试工具,步骤是先用selenium打开视频页面,再点击播放按钮,将此时已经刷新完的网页源代码保存,

再从中提取到视频的真正网址,最后再将调试好的webdriver设置为无界面模式
实现代码:

from selenium import webdriver from lxml import etree from selenium.webdriver.chrome.options import Options import requests import json import time class Douyin: def page_num(self,max_cursor): #网址后面的随机参数(我实在分析不出规律) # 设置谷歌无界面浏览器 chrome_options = Options() chrome_options.add_argument(‘–headless’) chrome_options.add_argument(‘–disable-gpu’) # chromdriver地址 path = r’/home/jmhao/chromedriver’ #随机码 random_field = ‘IU4uXRAbf-iiAwnGoS-puCFOLk&dytk=a61cb3ce173fbfa0465051b2a6a9027e’ #网址的主体 url = ‘https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=MS4wLjABAAAAF5ZfVgdRbJ3OPGJPMFHnDp2sdJaemZo3Aw6piEtkdOA&count=21&max_cursor=’ + str(max_cursor) + ‘&aid=1128&_signature=’ + random_field #请求头 headers = { ‘user-agent’:‘Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36’, } response = requests.get(url,headers=headers).text #转换成json数据 resp = json.loads(response) #提取到max_cursor max_cursor = resp[‘max_cursor’] #遍历 for data in resp[“aweme_list”]: # id值 video_id = data[‘aweme_id’] # 视频简介 video_title = data[‘desc’] # 构造视频网址 video_url = ‘https://www.iesdouyin.com/share/video/{}/?mid=1’ # 填充内容 video_douyin = video_url.format(video_id) driver = webdriver.Chrome(executable_path=path, options=chrome_options) # 打开视频界面 driver.get(video_douyin) # 点击播放按钮 driver.find_element_by_class_name(‘play-btn’).click() time.sleep(2) # 将网页源码存放到变量中 information = driver.page_source # 退出 driver.quit() html = etree.HTML(information) # 提取视频地址 video_adress = html.xpath(“//video[@class=’player’]/@src”) print(video_adress) #判断停止构造网址的条件 if max_cursor==0: return 1 else: #否则循环构造网址 douyin.page_num(max_cursor) if __name__ == ‘__main__’: douyin = Douyin() douyin.page_num(max_cursor=0)

打印一下视频的真实网址:

下载视频

视频的真实网址我们已经获得了,接下来只剩下最后一步的操作了—->下载视频
视频下载的操作就非常简单了:

for i in video_adress: #请求视频 video = requests.get(i,headers=headers).content with open(‘douyin/’ + video_title,‘wb’) as f: print(‘正在下载:’,video_title) f.write(video)

全部代码

from selenium import webdriver from lxml import etree from selenium.webdriver.chrome.options import Options import requests import json import time class Douyin: def page_num(self,max_cursor): #网址后面的随机参数(我实在分析不出规律) # 设置谷歌无界面浏览器 chrome_options = Options() chrome_options.add_argument(‘–headless’) chrome_options.add_argument(‘–disable-gpu’) # chromdriver地址 path = r’/home/jmhao/chromedriver’ #随机码 random_field = ‘yo91eRAflEhJwlLiO2coYsqPdW&dytk=4a01c95562f1f10264fb14086512f919’ #网址的主体 url = ‘https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=MS4wLjABAAAAU7Bwg8WznVaafqWLyLUwcVUf9LgrKGYmctJ3n5SwlOA&count=21&max_cursor=’ + str(max_cursor) + ‘&aid=1128&_signature=’ + random_field #请求头 headers = { ‘user-agent’:‘Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36’, } response = requests.get(url,headers=headers).text #转换成json数据 resp = json.loads(response) #提取到max_cursor max_cursor = resp[‘max_cursor’] #遍历 for data in resp[“aweme_list”]: # id值 video_id = data[‘aweme_id’] # 视频简介 video_title = data[‘desc’] # 构造视频网址 video_url = ‘https://www.iesdouyin.com/share/video/{}/?mid=1’ # 填充内容 video_douyin = video_url.format(video_id) driver = webdriver.Chrome(executable_path=path, options=chrome_options) # 打开视频界面 driver.get(video_douyin) # 点击播放按钮 driver.find_element_by_class_name(‘play-btn’).click() time.sleep(2) # 将网页源码存放到变量中 information = driver.page_source # 退出 driver.quit() html = etree.HTML(information) # 提取视频地址 video_adress = html.xpath(“//video[@class=’player’]/@src”) for i in video_adress: # 请求视频 video = requests.get(i, headers=headers).content with open(‘douyin/’ + video_title, ‘wb’) as f: print(‘正在下载:’, video_title) f.write(video) #判断停止构造网址的条件 if max_cursor==0: return 1 else: douyin.page_num(max_cursor) return url if __name__ == ‘__main__’: douyin = Douyin() douyin.page_num(max_cursor=0)

实现结果

可以看到这些视频已经下载到本地了,我们在打开本地文件夹看一下

随便打开一个视频文件:

可以播放!至此我这个问题版的抖音爬虫就做完了

待解决的问题

  • 如何获取主页中所有的id地址
  • 为什么请求的url后缀会一直变化,该怎么破解

其实所有的问题都指向了一个地方:该怎么获取小视频的id

如果大佬们有更好的方法可以获取id值,希望大佬们可以提出建议让我把这个爬虫的功能再完善!感谢各位!

C# 集合-并发处理-锁OR线程 - 天才卧龙 - 博客园

mikel阅读(520)

来源: C# 集合-并发处理-锁OR线程 – 天才卧龙 – 博客园

每次写博客,第一句话都是这样的:程序员很苦逼,除了会写程序,还得会写博客!当然,希望将来的一天,某位老板看到此博客,给你的程序员职工加点薪资吧!因为程序员的世界除了苦逼就是沉默。我眼中的程序员大多都不爱说话,默默承受着编程的巨大压力,除了技术上的交流外,他们不愿意也不擅长和别人交流,更不乐意任何人走进他们的内心!

最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来。我们都知道计算机技术发展日新月异,速度惊人的快,你我稍不留神,就会被慢慢淘汰!因此:每日不间断的学习是避免被淘汰的不二法宝。

当然,题外话说多了,咱进入正题!

简单的总结下对预防并发的理解:预防并发其实就是将并行执行修改为串行执行。(关于数据库并发问题大家可参考我的博客:C# 数据库并发的解决方案(通用版、EF版)

背景

C#命名空间:System.Collenctions和System.Collenctions.Generic 中提供了很多列表、集合和数组。例如:List<T>集合,数组Int[],String[] ……,Dictory<T,T>字典等等。但是这些列表、集合和数组的线程都不是安全的,不能接受并发请求。下面通过一个例子来加以说明,如下:

复制代码
 class Program
    {
        private static object o = new object();
        private static List<Product> _Products { get; set; }
        /*  coder:天才卧龙  
         *  代码中 创建三个并发线程 来操作_Products 集合
         *  System.Collections.Generic.List 这个列表在多个线程访问下,不能保证是安全的线程,所以不能接受并发的请求,我们必须对ADD方法的执行进行串行化
         */
        static void Main(string[] args)
        {
            _Products = new List<Product>();
            /*创建任务 t1  t1 执行 数据集合添加操作*/
            Task t1 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t2  t2 执行 数据集合添加操作*/
            Task t2 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t3  t3 执行 数据集合添加操作*/
            Task t3 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            Task.WaitAll(t1, t2, t3);
            Console.WriteLine(_Products.Count);
            Console.ReadLine();
        }

        /*执行集合数据添加操作*/
        static void AddProducts()
        {
            Parallel.For(0, 1000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                _Products.Add(product);
            });

        }
    }

    class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public int SellPrice { get; set; }
    }
复制代码

本例中,开辟了三个线程,通过循环向集合中添加数据,每个线程执行1000次(三个线程之间的操作是同时进行的,也是并行的),那么,理论上结果应该是3000。

上文中我们讲到: C#命名空间:System.Collenctions和System.Collenctions.Generic 下的列表,数组,集合并不能保证线程安全,并不能防止并发的发生。

本例运行的结果也证明了上述结论的正确性,其结果如下:

由此可见:C#命名空间:System.Collenctions和System.Collenctions.Generic 下的列表,数组,集合确实不能保证线程安全,确实不能预防并发。那么我们应当怎么解决上述问题呢?

还好,自C#2.0以来,LOCK是一直存在的。使用LOCK(互斥锁)是可以做到防止并发的,示例代码如下:

复制代码
 class Program
    {
        private static object o = new object();
        private static List<Product> _Products { get; set; }
        /*  coder:天才卧龙  
         *  代码中 创建三个并发线程 来操作_Products 集合
         *  System.Collections.Generic.List 这个列表在多个线程访问下,不能保证是安全的线程,所以不能接受并发的请求,我们必须对ADD方法的执行进行串行化
         */
        static void Main(string[] args)
        {
            _Products = new List<Product>();
            /*创建任务 t1  t1 执行 数据集合添加操作*/
            Task t1 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t2  t2 执行 数据集合添加操作*/
            Task t2 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t3  t3 执行 数据集合添加操作*/
            Task t3 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            Task.WaitAll(t1, t2, t3);
            Console.WriteLine(_Products.Count);
            Console.ReadLine();
        }

        /*执行集合数据添加操作*/
        static void AddProducts()
        {
            Parallel.For(0, 1000, (i) =>
               {
                   lock (o)
                   {

                       Product product = new Product();
                       product.Name = "name" + i;
                       product.Category = "Category" + i;
                       product.SellPrice = i;
                       _Products.Add(product);
                   }
               });
        }
    }

    class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public int SellPrice { get; set; }
    }
复制代码

引入了Lock,运行结果也正常了,如下:

但是锁的引入,带来了一定的开销和性能的损耗,并降低了程序的扩展性,而且还会有死锁的发生(虽说概率不大,但也不能不防啊),因此:使用LOCK进行并发编程显然不太适用。

还好,微软一直在更新自己的东西:

.NET Framework 4提供了新的线程安全和扩展的并发集合,它们能够解决潜在的死锁问题和竞争条件问题,因此在很多复杂的情形下它们能够使得并行代码更容易编写,这些集合尽可能减少使用锁的次数,从而使得在大部分情形下能够优化为最佳性能,不会产生不必要的同步开销。

需要注意的是:在串行代码中使用并发集合是没有意义的,因为它们会增加无谓的开销。

在.NET Framework4.0以后的版本中提供了命名空间:System.Collections.Concurrent 来解决线程安全问题,通过这个命名空间,能访问以下为并发做好了准备的集合。

   1.BlockingCollection 与经典的阻塞队列数据结构类似,能够适用于多个任务添加和删除数据,提供阻塞和限界能力。

   2.ConcurrentBag 提供对象的线程安全的无序集合

   3.ConcurrentDictionary  提供可有多个线程同时访问的键值对的线程安全集合

   4.ConcurrentQueue   提供线程安全的先进先出集合

   5.ConcurrentStack   提供线程安全的后进先出集合

这些集合通过使用比较并交换和内存屏障等技术,避免使用典型的互斥重量级的锁,从而保证线程安全和性能。

   ConcurrentQueue 

ConcurrentQueue 是完全无锁的,能够支持并发的添加元素,先进先出。下面贴代码,详解见注释:

复制代码
class Program
    {
        private static object o = new object();
        /*定义 Queue*/
        private static Queue<Product> _Products { get; set; }
        private static ConcurrentQueue<Product> _ConcurrenProducts { get; set; }
        /*  coder:天才卧龙  
         *  代码中 创建三个并发线程 来操作_Products 和 _ConcurrenProducts 集合,每次添加 10000 条数据 查看 一般队列Queue 和 多线程安全下的队列ConcurrentQueue 执行情况
         */
        static void Main(string[] args)
        {
            Thread.Sleep(1000);
            _Products = new Queue<Product>();
            Stopwatch swTask = new Stopwatch();//用于统计时间消耗的
            swTask.Start();

            /*创建任务 t1  t1 执行 数据集合添加操作*/
            Task t1 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t2  t2 执行 数据集合添加操作*/
            Task t2 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t3  t3 执行 数据集合添加操作*/
            Task t3 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });

            Task.WaitAll(t1, t2, t3);
            swTask.Stop();
            Console.WriteLine("List<Product> 当前数据量为:" + _Products.Count);
            Console.WriteLine("List<Product> 执行时间为:" + swTask.ElapsedMilliseconds);

            Thread.Sleep(1000);
            _ConcurrenProducts = new ConcurrentQueue<Product>();
            Stopwatch swTask1 = new Stopwatch();
            swTask1.Start();

            /*创建任务 tk1  tk1 执行 数据集合添加操作*/
            Task tk1 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*创建任务 tk2  tk2 执行 数据集合添加操作*/
            Task tk2 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*创建任务 tk3  tk3 执行 数据集合添加操作*/
            Task tk3 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });

            Task.WaitAll(tk1, tk2, tk3);
            swTask1.Stop();
            Console.WriteLine("ConcurrentQueue<Product> 当前数据量为:" + _ConcurrenProducts.Count);
            Console.WriteLine("ConcurrentQueue<Product> 执行时间为:" + swTask1.ElapsedMilliseconds);
            Console.ReadLine();
        }

        /*执行集合数据添加操作*/

        /*执行集合数据添加操作*/
        static void AddProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                lock (o)
                {
                    _Products.Enqueue(product);
                }
            });

        }
        /*执行集合数据添加操作*/
        static void AddConcurrenProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                _ConcurrenProducts.Enqueue(product);
            });

        }
    }

    class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public int SellPrice { get; set; }
    }
复制代码

结果如下:

从执行时间上来看,使用 ConcurrentQueue 相比 LOCK 明显快了很多!

   1.BlockingCollection 与经典的阻塞队列数据结构类似,能够适用于多个任务添加和删除数据,提供阻塞和限界能力。

   2.ConcurrentBag 提供对象的线程安全的无序集合

   3.ConcurrentDictionary  提供可有多个线程同时访问的键值对的线程安全集合

   4.ConcurrentQueue   提供线程安全的先进先出集合

   5.ConcurrentStack   提供线程安全的后进先出集合

上面的实例可以使用ConcurrentBag吗?当然是可以的啦,因为:ConcurrentBag 和 ConcurrentQueue一样,操作的对象都是集合,只不过方式不同罢了!同理:小虎斑们也可以尝试使用 ConcurrentStack 在这里,我仅仅贴上使用ConcurrentBag的代码,如下:

复制代码
 class Program
    {
        private static object o = new object();
        /*定义 Queue*/
        private static Queue<Product> _Products { get; set; }
        private static ConcurrentBag<Product> _ConcurrenProducts { get; set; }
        /*  coder:天才卧龙  
         *  代码中 创建三个并发线程 来操作_Products 和 _ConcurrenProducts 集合,每次添加 10000 条数据 查看 一般队列Queue 和 多线程安全下的队列ConcurrentQueue 执行情况
         */
        static void Main(string[] args)
        {
            Thread.Sleep(1000);
            _Products = new Queue<Product>();
            Stopwatch swTask = new Stopwatch();//用于统计时间消耗的
            swTask.Start();

            /*创建任务 t1  t1 执行 数据集合添加操作*/
            Task t1 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t2  t2 执行 数据集合添加操作*/
            Task t2 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*创建任务 t3  t3 执行 数据集合添加操作*/
            Task t3 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });

            Task.WaitAll(t1, t2, t3);
            swTask.Stop();
            Console.WriteLine("List<Product> 当前数据量为:" + _Products.Count);
            Console.WriteLine("List<Product> 执行时间为:" + swTask.ElapsedMilliseconds);

            Thread.Sleep(1000);
            _ConcurrenProducts = new ConcurrentBag<Product>();
            Stopwatch swTask1 = new Stopwatch();
            swTask1.Start();

            /*创建任务 tk1  tk1 执行 数据集合添加操作*/
            Task tk1 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*创建任务 tk2  tk2 执行 数据集合添加操作*/
            Task tk2 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*创建任务 tk3  tk3 执行 数据集合添加操作*/
            Task tk3 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });

            Task.WaitAll(tk1, tk2, tk3);
            swTask1.Stop();
            Console.WriteLine("ConcurrentQueue<Product> 当前数据量为:" + _ConcurrenProducts.Count);
            Console.WriteLine("ConcurrentBag<Product> 执行时间为:" + swTask1.ElapsedMilliseconds);
            Console.ReadLine();
        }

        /*执行集合数据添加操作*/

        /*执行集合数据添加操作*/
        static void AddProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                lock (o)
                {
                    _Products.Enqueue(product);
                }
            });

        }
        /*执行集合数据添加操作*/
        static void AddConcurrenProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                _ConcurrenProducts.Add(product);
            });

        }
    }

    class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public int SellPrice { get; set; }
    }
复制代码

执行结果如下:

对于并发下的其他集合,我这边就不做代码案列了!如有疑问,欢迎指正!

@陈卧龙的博客

SQL-乐观锁,悲观锁之于并发 - 天才卧龙 - 博客园

mikel阅读(503)

来源: SQL-乐观锁,悲观锁之于并发 – 天才卧龙 – 博客园

每次写博客,第一句话都是这样的:程序员很苦逼,除了会写程序,还得会写博客!当然,希望将来的一天,某位老板看到此博客,给你的程序员职工加点薪资吧!因为程序员的世界除了苦逼就是沉默。我眼中的程序员大多都不爱说话,默默承受着编程的巨大压力,除了技术上的交流外,他们不愿意也不擅长和别人交流,更不乐意任何人走进他们的内心!

最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来。我们都知道计算机技术发展日新月异,速度惊人的快,你我稍不留神,就会被慢慢淘汰!因此:每日不间断的学习是避免被淘汰的不二法宝。

当然,题外话说多了,咱进入正题!

引言
为什么需要锁(并发控制)?

  在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。

典型的冲突有:

  • 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
  • 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。

为了解决这些并发带来的问题。 我们需要引入并发控制机制。

并发控制机制

  悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1]

  乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。

最常用的处理多用户并发访问的方法是加锁。当一个用户锁住数据库中的某个对象时,其他用户就不能再访问该对象。加锁对并发访问的影响体现在锁的粒度上。比如,放在一个表上的锁限制对整个表的并发访问;放在数据页上的锁限制了对整个数据页的访问;放在行上的锁只限制对该行的并发访问。可见行锁粒度最小,并发访问最好,页锁粒度最大,并发访问性能就会越低。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1] 悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的锁定一个对象,限制其他用户的访问,也就是说悲观锁的并发访问性不好。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发用户读取对象的次数。

从数据库厂商的角度看,使用乐观的页锁是比较好的,尤其在影响很多行的批量操作中可以放比较少的锁,从而降低对资源的需求提高数据库的性能。再考虑聚集索引。在数据库中记录是按照聚集索引的物理顺序存放的。如果使用页锁,当两个用户同时访问更改位于同一数据页上的相邻两行时,其中一个用户必须等待另一个用户释放锁,这会明显地降低系统的性能。interbase和大多数关系数据库一样,采用的是乐观锁,而且读锁是共享的,写锁是排他的。可以在一个读锁上再放置读锁,但不能再放置写锁;你不能在写锁上再放置任何锁。锁是目前解决多用户并发访问的有效手段。

综上所述:在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.

悲观锁应用

需要使用数据库的锁机制,比如SQL SERVER 的TABLOCKX(排它表锁) 此选项被选中时,SQL  Server  将在整个表上置排它锁直至该命令或事务结束。这将防止其他进程读取或修改表中的数据。

SQLServer中使用

Begin Tran
select top 1 @TrainNo=T_NO
from Train_ticket   with (UPDLOCK)   where S_Flag=0

update Train_ticket
set T_Name=user,
T_Time=getdate(),
S_Flag=1
where T_NO=@TrainNo
commit

我们在查询的时候使用了with (UPDLOCK)选项,在查询记录的时候我们就对记录加上了更新锁,表示我们即将对此记录进行更新. 注意更新锁和共享锁是不冲突的,也就是其他用户还可以查询此表的内容,但是和更新锁和排它锁是冲突的.所以其他的更新用户就会阻塞.

在此:举个简单的例子来说明悲观锁的应用,我们以SQLServer为例进行说明:

假如两个线程同时修改数据库同一条记录,就会导致后一条记录覆盖前一条,从而引发一些问题。

例如:

一个售票系统有一个余票数,客户端每调用一次出票方法,余票数就减一。

情景:

总共300张票,假设两个售票点,恰好在同一时间出票,它们做的操作都是先查询余票数,然后减一。

一般的sql语句:

问题就在于,同一时间获取的余票都为300,每个售票点都做了一次更新为299的操作,导致余票少了1,而实际出了两张票。

打开两个查询窗口,分别快速运行以上代码即可看到效果。

因此:在此我们可以采用悲观锁进行解决

在查询的时候加了一个更新锁,保证自查询起直到事务结束不会被其他事务读取修改,避免产生脏数据。

从而可以解决上述问题。

如果这个售票系统并发太高(例如:春节售票,十月一国庆售票等买票人员并发操作很高),我们采用上述的悲观锁方案就会是系统性能大大降低。譬如:售票员A锁定了这个操作,售票员A在处理这个业务的过程中,又去喝了杯咖啡,在此期间,其他业务员是无法进行订票的,所以…

乐观锁解决方案:

这便是乐观锁的解决方案,可以解决并发带来的数据错误问题,但不保证每一次调用更新都成功,可能会返回’更新失败’

 

悲观锁和乐观锁

悲观锁一定成功,但在并发量特别大的时候会造成很长堵塞甚至超时,仅适合小并发的情况。

乐观锁不一定每次都修改成功,但能充分利用系统的并发处理机制,在大并发量的时候效率要高很多。

下面以SQLServer为例,详情说明悲观锁和乐观锁(请务必看懂关于乐观锁的实现,因为:在实际的系统中,乐观锁应用较为广泛)

锁( locking )
业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。
Hibernate 支持两种锁机制:即通常所说的 “ 悲观锁( Pessimistic Locking ) ”和 “ 乐观锁( Optimistic Locking ) ” 。
悲观锁( Pessimistic Locking )
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
一个典型的倚赖数据库的悲观锁调用:
select * from account where name=”Erica” for update
这条 sql 语句锁定了 account 表中所有符合检索条件( name=”Erica” )的记录。
本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
Hibernate 的悲观锁,也是基于数据库的锁机制实现。

Hibernate 的加锁模式有: 
Ø LockMode.NONE : 无锁机制。
Ø LockMode.WRITE : Hibernate 在 Insert 和 Update 记录的时候会自动获取。
Ø LockMode.READ : Hibernate 在读取记录的时候会自动获取。
乐观锁( Optimistic Locking ) 
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依 靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进 行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。

乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本 Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于(数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
2 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
3 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现.
需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

当然:除了SQL上的锁可以解决数据并发外,我们也可以结合编程语言来实现并发的控制。

在此:以C#为例,我们可以通过代码临界区的定义来实现并发的控制。在C#语言中,定义一个代码临界区使用的关键字是LOCK,在此,小弟不作详细介绍,仅仅作为提示大家,有兴趣的小伙伴可以自行查阅关于C#临界区代码关键字LOCK的使用,本人系列博客:模拟并发/C# 并发处理 锁OR线程

中对LOCK关键字作了部分讲解,有兴趣的小虎斑可以参考下,谢谢、

@陈卧龙的博客 

SQL Server中灾难时备份结尾日志(Tail of log)的两种方法 - CareySon - 博客园

mikel阅读(465)

来源: SQL Server中灾难时备份结尾日志(Tail of log)的两种方法 – CareySon – 博客园

简介

 

在数据库数据文件因各种原因发生损坏时,如果日志文件没有损坏。可以通过备份结尾日志(Tail of log)使得数据库可以恢复到灾难发生时的状态。

例如:

grid.ai

上图中。在DB_1中做了完整备份,在Log_1,Log_2处做了日志备份。在Log_2备份之后不久,发生了故障。从Log_2备份到灾难发生时之间的日志。就是结尾日志(Tail of log)。如果不能备份尾端日志,则数据库只能恢复到Log_2备份的点。尾端日志期间所做的改动全部丢失。更详细的概念可以查看我之前关于日志的博文。

下面我们分别来看在SQL Server实例运行良好和SQL Server实例崩溃状态下,备份结尾日志方法。

SQL Server实例运行正常时,结尾日志的备份

下面来模拟一次灾难下结尾日志的备份:

1

现在数据库TestBackUp有了一个完整备份和一个日志备份,而最后那条”日志备份后的测试数据”是在上次日志备份之后的,被结尾日志所包含。

接下来模拟数据库文件所在磁盘损坏(日志文件完好)

1.停掉Server SQL服务

2.删除数据库文件(MDF文件)

 

此时在SSMS中访问数据库TestBackUp会出现不可用:

2

此时,因为SQL Server实例可用,通过在T-SQL语句指定NO_TRUNCATE选项(必须指定,否则无法备份尾端日志),备份尾端日志:

3

依次进行完整备份恢复,和两次事务日志恢复,可以看到数据已经成功恢复到灾难点:

4

 

当SQL Server实例崩溃时,结尾日志的备份

此时由于各种原因,所处的SQL Server实例也崩溃,无法通过T-SQL来备份结尾日志。此时数据库文件损坏,而事务日志文件保持正确。

假设情况和上面例子一样,此时我手里有一个完整备份(TestBackUp_FULL.bak)和一个日志备份(TestBackUp_log1.bak),还有一个日志文件(ldf)。

这时我将这几个文件拷贝到其他拥有SQL Server实例的机器上。

新建一个和原数据库名一样的数据库。设置为脱机:

5

删除新建数据库的MDF文件。

将需要备份的数据库的日志文件替换掉原有的LDF文件。

此时直接备份结尾日志,成功:

6

原有Sql server实例恢复后一次恢复完整备份和两个日志备份。成功恢复到灾难发生点。

 

总结

我相信看到这篇文章的人都不希望碰到用到上面两种方法的情况。但是,墨菲定律(事情如果有变坏的可能,无论这种可能性有多小,它总会发生)是残酷的,事先练习一下总是比真正遇到情况用生产数据练习惬意的多:-)

浅谈SQL Server中的事务日志(四)----在完整恢复模式下日志的角色 - CareySon - 博客园

mikel阅读(589)

来源: 浅谈SQL Server中的事务日志(四)—-在完整恢复模式下日志的角色 – CareySon – 博客园

本篇文章是系列文章中的第四篇,也是最后一篇,本篇文章需要前三篇的文章知识作为基础,前三篇的文章地址如下:

浅谈SQL Server中的事务日志(一)—-事务日志的物理和逻辑构架

浅谈SQL Server中的事务日志(二)—-事务日志在修改数据时的角色

浅谈SQL Server中的事务日志(三)—-在简单恢复模式下日志的角色

 

简介

生产环境下的数据是如果可以写在资产负债表上的话,我想这个资产所占的数额一定不会小。而墨菲定律(事情如果有变坏的可能,无论这种可能性有多小,它总会发生)仿佛是给DBA量身定做的。在上篇文章介绍的简单恢复模式下,从最近一次备份到当前的数据都会存在丢失的风险。而完整备份模式使得数据丢失的风险大大减少。本文主要介绍在完整备份模式下概念原理和日志所处的角色。

 

完整(Full)恢复模式

完整恢复模式通过将对数据库的任何修改记录到日志来给予数据最大程度的保护。在完整恢复模式下,日志的作用不仅仅是保证了数据库事务的ACID。并且还可以使数据恢复到在日志范围内的任何时间点。

在上一篇文章中说过,在简单恢复模式下,日志几乎是不用进行管理的。每一次CheckPoint都有可能截断日志,从而来回收不活动的VLF以便重复利用空间。因此在简单恢复模式下,日志的空间使用几乎可以不去考虑。与之相反,在完整恢复模式下,日志作为恢复数据的重要组成部分,日志的管理和对日志空间使用的管理则需要重视。

在完整恢复模式下,CheckPoint不会截断日志。只有对日志的备份才会将MinLSN向后推并截断日志。因此在一个业务量稍大的系统中,日志的增长速度将会变得很快。

因此日志备份的目的分为以下两个:

  •     减少活动日志的大小
  •     减少日志损坏的风险

通过从MSDN中摘自的下图可以看到:

 

grid.ai

 

在DB_1处做了完整备份,并且接下来两次分别做了两次日志备份(Log_1和Log_2),在Log_2备份完不久服务器由于数据所在磁盘损坏。这时如果日志文件完好,则可以通过备份尾部日志(Tail of log)后,从DB_1开始恢复,依次恢复Log_1,Log_2,尾部日志来将数据库恢复到灾难发生时的时间点。理论上可以使数据的损失为0。

从日志恢复数据的原理是Redo,也就是将日志中记载的事务再重做一遍。这个开销和从完整或差异备份中恢复相比,要大很多。因此尽可能的减少利用日志的恢复量。而使用完整或者差异备份来恢复更多的数据。

 

大容量日志(Bulk-logged)恢复模式

大容量恢复模式在很多地方和完整恢复模式相同。但由于在完整恢复模式下,对数据库的每一项操作都会记录在日志中。而对于某些大量数据的导入导出操作,无疑会在日志中留下大量记录。很多情况下,我们并不需要将这些信息记录在日志中。

而大容量日志恢复模式作为完整恢复模式的备选方案。微软推荐的最佳实践是在进行大量数据操作时(比如索引的创建和rebuilt,select into操作等),暂时由完整恢复模式切换到大容量恢复模式来节省日志。这个转换并不会破坏日志链。

本文不会深入探讨这个模式,仅仅是对这个概念做简单解释。假设我要插入一批数据,则完整恢复模式和大容量日志恢复模式在日志中所记录的信息如下:

1

由此可以看出,在日志中,大容量恢复模式将这类操作变为一个原子。

 

日志链(Log Chain)

连续的日志备份被称之为日志链。表示日志是连续的.这个概念可以用下图表示:

2

假设上面两个日志备份可以简单抽象成如上2个备份,则日志备份1的末尾LSN必须小于等于日志备份二的第一个LSN(通常情况下是第一个末尾LSN等于第二个日志备份的第一个LSN,但由于存在“只备份日志”选项只备份日志,并不截断日志,所以有可能重叠)。则这两个备份的日志链是连续的。

下图是一个生产环境下,在SSMS中查看日志链连续的例子:

3

可以看出,第一次完整备份后,备份多次事务日志,每一个事务日志的开始LSN都等于上一个事务日志的结束LSN。因此可以从第一次完整备份开始,恢复到最后一个日志备份期间的任何时间点。

 

完整的日志链以第一次完整备份或由简单恢复模式转为完整或大容量日志模式开始,到当前的时间点结束。

而从日志恢复数据要求从最近一次完整或差异备份到所恢复的时间点之间的日志链是连续的。

 

恢复次序

从备份恢复数据需要经历如下几步骤:

1.复制数据阶段:从完整备份和差异备份中将数据,索引页和日志复制到被恢复数据库文件。

2.Redo(roll forward)阶段:将记录在日志中的事务应用到从备份中复制过来的数据。使数据Roll Forward到指定的时间点.这个阶段完成后,数据库还处于不可使用阶段:

如图: 4

上面两部就是Restore

3.Undo(Roll Back)阶段:这也是传说中的Recovery,将任何未提交的事务回滚。这个阶段过后,数据库处于可用状态。任何后续备份将不能接着应用到当前数据库。

这个概念比如:

在连续两个日志链的日志备份,在第一个事务日志备份中定义的事务,在第二个事务日志备份中Commit.如果在第一个事务日志还原后使用了Recovery选项.也就是经历了Undo阶段。则事务1在Undo阶段会被回滚:

5

可见,日志备份1中的T1被回滚,在日志备份2中的Commit也就毫无意义。这也就是为什么经历过Undo阶段后不允许再恢复后续备份。因此,微软推荐的最佳实践是使用NoRecovery选项不进行Undo阶段。而在所有备份恢复后单独进行Undo阶段,这个操作可以通过还原日志尾部时,指定Recovery选项进行。

 

总结

本文简单介绍了在完整恢复模式下,日志的作用以及对数据恢复的一些概念。理解完整恢复模式的概念对于减少数据丢失的风险是无可替代的。

浅谈SQL Server中的事务日志(三)----在简单恢复模式下日志的角色 - CareySon - 博客园

mikel阅读(509)

来源: 浅谈SQL Server中的事务日志(三)—-在简单恢复模式下日志的角色 – CareySon – 博客园

本篇文章是系列文章中的第三篇,前两篇的地址如下:

浅谈SQL Server中的事务日志(一)—-事务日志的物理和逻辑构架

浅谈SQL Server中的事务日志(二)—-事务日志在修改数据时的角色

 

简介

在简单恢复模式下,日志文件的作用仅仅是保证了SQL Server事务的ACID属性。并不承担具体的恢复数据的角色。正如”简单”这个词的字面意思一样,数据的备份和恢复仅仅是依赖于手动备份和恢复.在开始文章之前,首先要了解SQL Server提供的几种不同备份类型。

 

SQL Server提供的几种备份类型

SQL Server所提供的几种备份类型基本可以分为以下三种(文件和文件组备份以及部分备份不在本文讨论之列):

1.完整(Full)备份:直接将所备份的数据的所有区(Extent)进行复制。这里值得注意的有2点:

  •     完整备份并不像其名字“完整”那样备份所有部分,而是仅备份数据库本身,而不备份日志(虽然仅仅备份少量日志用于同步)
  •     完整备份在备份期间,数据库是可用的。完整备份会记录开始备份时的MinLSN号,结束备份时的LSN号,将这个区间的日志进行备份,在恢复时应用到被恢复的数据库(这里经过修改,感谢魔君六道指出)

 

2.差异(Differential)备份:只备份上次完整备份后,做修改的部分。备份单位是区(Extent)。意味着某个区内即使只有一页做了变动,则在差异备份里会被体现.差异备份依靠一个BitMap进行维护,一个Bit对应一个区,自上次完整备份后,被修改的区会被置为1,而BitMap中被置为1对应的区会被差异备份所备份。而到下一次完整备份后,BitMap中所有的Bit都会被重置为0。

 

3.日志(Log)备份:仅仅备份自上次完整备份或日志备份之后的记录。在简单模式下,日志备份毫无意义(SQL Server不允许在简单恢复模式下备份日志),下文会说明在简单恢复模式下,为什么日志备份没有意义。

 

简单恢复模式(Simple Recovery Mode)

 

在简单恢复模式下,日志仅仅是为了保证SQL Server事务的ACID。并没有恢复数据的功能.

比如,我们有一个备份计划,如下:

1

我们在每周一0点做一次完整备份,在周三0点和周五0点分别做差异备份。在简单恢复模式下,如果周六数据库崩溃。我们的恢复计划只有根据周一0点的做的完整备份恢复后,再利用周五0点的差异备份进行恢复.而周五0点之后到服务器崩溃期间所有的数据将会丢失。

正如”简单”这个词所涵盖的意思,在简单恢复模式下,日志可以完全不用管理。而备份和恢复完全依赖于我们自己的完整和差异备份.

恢复模式是一个数据库级别的参数,可以通过在SSMS里或通过SQL语句进行配置:

2

 

 

简单恢复模式下日志的空间使用

在本系列文章的第一篇文章提到过,日志文件会划分成多个VLF进行管理,在逻辑上记录是线性的,给每个记录一个顺序的,唯一的LSN。

而在简单恢复模式下,为了保证事务的持久性,那些有可能回滚的数据会被写入日志。这些日志需要被暂时保存在日志以确保在特定条件下事务可以顺利回滚。这就涉及到了一个概念—最小恢复LSN(Minimum Recovery LSN(MinLSN) )

MinLsn是在还未结束的事务记录在日志中最小的LSN号,MinLSN是下列三者之一的最小值:

  • CheckPoint的开始LSN
  • 还未结束的事务在日志的最小LSN
  • 尚未传递给分发数据库的最早的复制事务起点的 LSN.

 

下图是一个日志的片段:

3

(图片摘自MSDN)

可以看到,最新的LSN是148,147是CheckPoint,在这个CheckPoint之前事务1已经完成,而事务2还未完成,所以对应的MinLSN应该是事务2的开始,也就是142.

而从MinLSN到日志的逻辑结尾处,则称为活动日志(Active Log)。

而活动日志分布在物理VLF上的关系可以用下图表示:

 

4

 

因此,VLF的状态是源自其上所含有的LSN的状态,可以分为两大类:活动VLF不活动VLF

    而更加细分可以将VLF的状态分为以下四类:

  1. 活动(Active) –在VLF 上存储的任意一条LSN是活动的时,则VLF则为活动状态,即使一个200M的VLF只包含了一条LSN,如上图的VLF3
  2. 可恢复(Recoverable) – VLF是不活动的,VLF上不包含活动LSN,但还未被截断(truncated)
  3. 可重用(Reusable) – VLF是不活动的,VLF上不包含活动LSN,已经被截断(truncated),可以重用
  4. 未使用(Unused) – VLF是不活动的,并且还未被使用过

概念如下图:

3

而所谓的截断(truncated)只是将可恢复状态的VLF转换到可重用状态。在简单恢复模式下,每一次CheckPoint,都会去检查是否有日志可以截断.如果有inactive的VLF时,CheckPoint都会将可截断部分进行截断,并将MinLSN向后推.

在日志达到日志文件(ldf文件)末尾时,也就是上图的VLF8时,会重新循环到VLF1开始,以便让空间进行重复利用.所以日志虽然可以从物理顺序上是从VLF1到VLF8,但逻辑顺序可以是从VLF6开始到VLF2结束:

5

因此可以看出,简单恢复模式下日志是不保存的(当事务结束后,相关的会被截断)。仅仅是用于保证事务回滚和崩溃恢复的用途.所以备份日志也就无从谈起,更不能利用日志来恢复数据库。

 

总结

本文介绍了简单恢复模式下日志的原理,并简单的引出了一些备份或者恢复数据的基础。而实际上,除了在开发或测试环境下。使用简单恢复模式的场景并不多,因为在现实生活中,在生产环境允许几个小时的数据丢失的场景几乎没有.下篇文章将会讲述在完整恢复模式下,日志的作用。

【译】一些优化你的SQL语句的TIPs - CareySon - 博客园

mikel阅读(636)

来源: 【译】一些优化你的SQL语句的TIPs – CareySon – 博客园

简介

对于写出实现功能的SQL语句和既能实现功能又能保证性能的SQL语句的差别是巨大的。很多时候开发人员仅仅是把精力放在实现所需的功能上,而忽略了其所写代码的性能和对SQL Server实例所产生的影响(也就是IO,CPU,内存方面的消耗).这甚至有可能使整个SQL Server实例跪了。本文旨在提供一些简单的步骤来帮助你优化SQL语句。

市面上已经有很多关于如何优化SQL Server性能的书籍和白皮书。所以本文并不打算达到那种深度和广度,而仅仅是为开发人员提供一个快速检测的列表来找到SQL语句中导致瓶颈产生的部分。

在开始解决性能问题之前,合适的诊断工具是必须的。除去众所周知的SSMS和SQL Profiler,SQL Server 2008还带有众多DMV来提供关键信息。本篇文章中,我将使用SSMS和一些DMV来找到SQL的瓶颈

 

那么,我们从哪开始

我的第一步是查看执行计划。这一步既可以通过SMSS也可以通过SQL Profiler实现,为了简便起见,我将在SMSS中获取执行计划。

1) 检查你是否忽略掉了某些表的连接的条件,从而导致了笛卡尔积(Cross)连接(Join)。比如,在生产系统中有两个表,每个表中有1000行数据。这其中绝大多数数据并不需要返回,如果你在这两个表上应用了Cross Join,返回的结果将会是100万行的结果集!返回如此数量的数据包括将所有数据从物理存储介质中读取出来,因而占用了IO。然后这些数据将会被导入内存,也就是SQL Server的缓冲区。这会将缓冲区内的其它页Flush出去。

 

2)查看你是否忽略了某些Where子句,缺少Where子句会导致返回额外不需要的行。这产生的影响和步骤一所产生的影响是一样的。

 

3)查看统计信息是否是自动创建和自动更新的,你可以在数据库的属性里看到这些选项

1

在默认条件下创建一个新数据库,Auto Create Statistics和Auto Update Statistics选项是开启的,统计信息是用于帮助查询优化器生成最佳执行计划的。这份白皮书对于解释统计信息的重要性以及对于执行计划的作用解释的非常到位。上面那些设置可以通过右键数据库,选择属性,在“选项”中找到。

 

4)检查统计信息是否已经过期,虽然统计信息是自动创建的,但是更新统计信息从而反映出数据的变化也同样重要。在一个大表中,有时候虽然Auto Update Statistics 选项已经开始,但统计信息依然无法反映出数据的分布情况。默认情况下,统计信息的更新是基于抽取表中的随机信息作为样本产生的。如果数据是按顺序存储的,那么很有可能数据样本并没有反映出表中的数据情况。因此,推荐在频繁更新的表中,统计信息使用Full Scan选项来定期更新。这种更新可以放到数据库闲时来做。

DBCC SHOW_STATISTICS命令可以用于查看上次统计信息的更新时间,行数以及样本行数.在这个例子中,我们可以看到Person.Address表上的AK_Address_rowguid索引的有关信息:

USE AdventureWorks;
GO
DBCC SHOW_STATISTICS ("Person.Address", AK_Address_rowguid);
GO

 

下面是输出结果,请注意Updated,Rows,Rows Sampled这三个列

2

 

如果你认为统计信息已经过期,则可以使用sp_updatestats这个存储过程来更新当前数据库中的所有统计信息:

 

或者使用FULLSCAN选项,则关于表Person.Address上的所有统计信息将会被更新:

UPDATE STATISTICS Person.Address WITH FULLSCAN

 

5)查看执行计划是否出现任何表或者索引的扫描(译者注:不是查找),在大多数情况下(这里假设统计信息是最新的),这意味着索引的缺失。下面几个DMV对于查找缺失索引很有帮助:

i) sys.dm_db_missing_index_details

ii) sys.dm_db_missing_index_group_stats

iii) sys.dm_db_missing_index_groups

接下来的几个语句使用了上面的DMV,按照索引缺失对于性能的影响,展现出信息:

SELECT avg_total_user_cost,avg_user_impact,user_seeks, user_scans,
ID.equality_columns,ID.inequality_columns,ID.included_columns,ID.statement 
FROM sys.dm_db_missing_index_group_stats GS
LEFT OUTER JOIN sys.dm_db_missing_index_groups IG On (IG.index_group_handle = GS.group_handle)
LEFT OUTER JOIN sys.dm_db_missing_index_details ID On (ID.index_handle = IG.index_handle)
ORDER BY avg_total_user_cost * avg_user_impact * (user_seeks + user_scans)DESC

 

你也可以使用数据引擎优化顾问来找出缺失的索引以及需要创建哪些索引来提高性能。

 

6)查看是否有书签查找,同样,在执行计划中找到书签查找十分容易,书签查找并不能完全避免,但是使用覆盖索引可以大大减少书签查找。

 

7)查看排序操作,如果在执行计划中排序操作占去了很大一部分百分比,我会考虑以下几种方案:

  •      按照所排序的列创建聚集索引,但这种方式一直存在争议。因为最佳实践是使用唯一列或者Int类型的列作为主键,然后让SQL Server在主键上创建聚集索引。但是在特定情况下使用排序列创建聚集索引也是可以的
  •     创建一个索引视图,在索引视图上按照排序列创建聚集索引
  •     创建一个排序列的非聚集索引,把其他需要返回的列INCLUDE进去

在我的另一篇文章中,我将会详细阐述选择最佳方案的方法。

 

8)查看加在表上的锁,如果所查的表由于一个DML语句导致上锁,则查询引擎需要花一些时间等待锁的释放。下面是一些解决锁问题的方法:

  •     让事务尽可能的短
  •     查看数据库隔离等级,降低隔离等级以增加并发
  •     在Select语句中使用表提示,比如READUNCOMMITTED 或 READPAST.虽然这两个表提示都会增加并发,但是ReadUnCommited可能会带来脏读的问题,而READPAST会只返回部分结果集

 

9)查看是否有索引碎片,索引碎片可以使用sys.dm_db_index_physical_statsDMV轻松查看,如果索引碎片已经大于30%,则推荐索引重建.而索引碎片小于30%时,推荐使用索引整理。索引碎片因为使查询需要读取更多的列从而增加了IO,而更多的页意味着占用更多的缓冲区,因此还会形成内存压力。

如下语句根据索引碎片的百分比查看所有索引:

Declare @db	SysName;
Set @db = '<DB NAME>';

SELECT CAST(OBJECT_NAME(S.Object_ID, DB_ID(@db)) AS VARCHAR(20)) AS 'Table Name',
 CAST(index_type_desc AS VARCHAR(20)) AS 'Index Type',
 I.Name As 'Index Name',
 avg_fragmentation_in_percent As 'Avg % Fragmentation',
 record_count As 'RecordCount',
 page_count As 'Pages Allocated',
 avg_page_space_used_in_percent As 'Avg % Page Space Used'
FROM sys.dm_db_index_physical_stats (DB_ID(@db),NULL,NULL,NULL,'DETAILED' ) S
LEFT OUTER JOIN sys.indexes I On (I.Object_ID = S.Object_ID and I.Index_ID = S.Index_ID)
AND S.INDEX_ID > 0
ORDER BY avg_fragmentation_in_percent DESC

 

下面语句可以重建指定表的所有索引:

ALTER INDEX ALL ON <Table Name> REBUILD;

 

下面语句可以重建指定索引:

ALTER INDEX <Index Name> ON <Table Name> REBUILD;

 

当然,我们也可以整理索引,下面语句整理指定表上的所有索引:

ALTER INDEX ALL ON <Table Name> REORGANIZE;

 

下面语句指定特定的索引进行整理:

ALTER INDEX <Index Name> ON <Table Name> REORGANIZE;

 

在重建或整理完索引之后,重新运行上面的语句来查看索引碎片的情况。

 

总结

上面的9个步骤并不是优化一个SQL语句必须的,尽管如此,你还是需要尽快找到是哪个步骤导致查询性能的瓶颈从而解决性能问题。就像文中开篇所说,性能的问题往往是由于更深层次的原因,比如CPU或内存压力,IO的瓶颈(这个列表会很长….),因此,更多的研究和阅读是解决性能问题所必须的。

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

原文链接:http://www.sqlservercentral.com/articles/Performance+Tuning/70647/

浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色 - CareySon - 博客园

mikel阅读(468)

来源: 浅谈SQL Server中的事务日志(二)—-事务日志在修改数据时的角色 – CareySon – 博客园

本篇文章是系列文章中的第二篇,以防你还没有看过第一篇.上一篇的文章地址如下:

浅谈SQL Server中的事务日志(一)—-事务日志的物理和逻辑构架

 

简介

每一个SQL Server的数据库都会按照其修改数据(insert,update,delete)的顺序将对应的日志记录到日志文件.SQL Server使用了Write-Ahead logging技术来保证了事务日志的原子性和持久性.而这项技术不仅仅保证了ACID中的原子性(A)和持久性(D),还大大减少了IO操作,把对数据的修改提交到磁盘的工作交给lazy-writer和checkpoint.本文主要讲述了SQL Server修改数据时的过程以及相关的技术。

 

预写式日志(Write-Ahead Logging (WAL))

SQL Server使用了WAL来确保了事务的原子性和持久性.实际上,不光是SQL Server,基本上主流的关系数据库包括oracle,mysql,db2都使用了WAL技术.

WAL的核心思想是:在数据写入到数据库之前,先写入到日志.

因为对于数据的每笔修改都记录在日志中,所以将对于数据的修改实时写入到磁盘并没有太大意义,即使当SQL Server发生意外崩溃时,在恢复(recovery)过程中那些不该写入已经写入到磁盘的数据会被回滚(RollBack),而那些应该写入磁盘却没有写入的数据会被重做(Redo)。从而保证了持久性(Durability)

但WAL不仅仅是保证了原子性和持久性。还会提高性能.

硬盘是通过旋转来读取数据,通过WAL技术,每次提交的修改数据的事务并不会马上反映到数据库中,而是先记录到日志.在随后的CheckPoint和lazy Writer中一并提交,如果没有WAL技术则需要每次提交数据时写入数据库:

1

而使用WAL合并写入,会大大减少磁盘IO:

2

     也许你会有疑问,那每次对于修改的数据还是会写入日志文件.同样消耗磁盘IO。上篇文章讲过,每一笔写入日志的记录都是按照先后顺序,给定顺序编号的LSN进行写入的,日志只会写入到日志文件的逻辑末端。而不像数据那样,可能会写到磁盘的各个地方.所以,写入日志的开销会比写入数据的开销小很多。

 

SQL Server修改数据的步骤

SQL Server对于数据的修改,会分为以下几个步骤顺序执行:

1.在SQL Server的缓冲区的日志中写入”Begin Tran”记录

2.在SQL Server的缓冲区的日志页写入要修改的信息

3.在SQL Server的缓冲区将要修改的数据写入数据页

4.在SQL Server的缓冲区的日志中写入”Commit”记录

5.将缓冲区的日志写入日志文件

6.发送确认信息(ACK)到客户端(SMSS,ODBC等)

 

可以看到,事务日志并不是一步步写入磁盘.而是首先写入缓冲区后,一次性写入日志到磁盘.这样既能在日志写入磁盘这块减少IO,还能保证日志LSN的顺序.

上面的步骤可以看出,即使事务已经到了Commit阶段,也仅仅只是把缓冲区的日志页写入日志,并没有把数据写入数据库.那将要修改的数据页写入数据库是在何时发生的呢?

 

Lazy Writer和CheckPoint

上面提到,SQL Server修改数据的步骤中并没有包含将数据实际写入到磁盘的过程.实际上,将缓冲区内的页写入到磁盘是通过两个过程中的一个实现:

这两个过程分别为:

1.CheckPoint

2.Lazy Writer

任何在缓冲区被修改的页都会被标记为“脏”页。将这个脏页写入到数据磁盘就是CheckPoint或者Lazy Writer的工作.

当事务遇到Commit时,仅仅是将缓冲区的所有日志页写入磁盘中的日志文件:

3

 

而直到Lazy Writer或CheckPoint时,才真正将缓冲区的数据页写入磁盘文件:

4

 

前面说过,日志文件中的LSN号是可以比较的,如果LSN2>LSN1,则说明LSN2的发生时间晚于LSN1的发生时间。CheckPoint或Lazy Writer通过将日志文件末尾的LSN号和缓冲区中数据文件的LSN进行对比,只有缓冲区内LSN号小于日志文件末尾的LSN号的数据才会被写入到磁盘中的数据库。因此确保了WAL(在数据写入到数据库之前,先写入日志)。

 

Lazy Writer和CheckPoint的区别

Lazy Writer和CheckPoint往往容易混淆。因为Lazy Writer和CheckPoint都是将缓冲区内的“脏”页写入到磁盘文件当中。但这也仅仅是他们唯一的相同点了。

Lazy Writer存在的目的是对缓冲区进行管理。当缓冲区达到某一临界值时,Lazy Writer会将缓冲区内的脏页存入磁盘文件中,而将未修改的页释放并回收资源。

而CheckPoint存在的意义是减少服务器的恢复时间(Recovery Time).CheckPoint就像他的名字指示的那样,是一个存档点.CheckPoint会定期发生.来将缓冲区内的“脏”页写入磁盘。但不像Lazy Writer,Checkpoint对SQL Server的内存管理毫无兴趣。所以CheckPoint也就意味着在这个点之前的所有修改都已经保存到了磁盘.这里要注意的是:CheckPoint会将所有缓冲区的脏页写入磁盘,不管脏页中的数据是否已经Commit。这意味着有可能已经写入磁盘的“脏页”会在之后回滚(RollBack).不过不用担心,如果数据回滚,SQL Server会将缓冲区内的页再次修改,并写入磁盘。

通过CheckPoint的运作机制可以看出,CheckPoint的间歇(Recovery Interval)长短有可能会对性能产生影响。这个CheckPoint的间歇是一个服务器级别的参数。可以通过sp_config进行配置,也可以在SSMS中进行配置:

5

恢复间歇的默认参数是0,意味着由SQL Server来管理这个回复间隔。而自己设置恢复间隔也是需要根据具体情况来进行界定。更短的恢复间歇意味这更短的恢复时间和更多的磁盘IO,而更长的恢复间歇则带来更少的磁盘IO占用和更长的恢复时间.

除了自动CheckPoint之外,CheckPoint还会发生在Alter DataBase以及关闭SQL Server服务器时。sysadmin和db_backupoperator组的成员以及db_owner也可以使用CheckPoint指令来手动保存CheckPoint:

6

通过指定CheckPoint后的参数,SQL Server会按照这个时间来完成CheckPoint过程,如果时间指定的短,则SQL Server会使用更多的资源优先完成CheckPoint过程。

通常情况下,将“脏”页写入磁盘的工作,Lazy Writer要做的比CheckPoint会多出许多。

 

总结

本文简单介绍了WAL的概念和修改数据库对象时,日志所扮演的角色。还分别介绍了CheckPoint和Lazy Writer,对于这些概念的理解是理解SQL Server DBA工作的基础。下篇文章将会讲述在简单恢复模式下日志的机制。

浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 - CareySon - 博客园

mikel阅读(591)

来源: 浅谈SQL Server中的事务日志(一)—-事务日志的物理和逻辑构架 – CareySon – 博客园

简介

SQL Server中的事务日志无疑是SQL Server中最重要的部分之一。因为SQL SERVER利用事务日志来确保持久性(Durability)和事务回滚(Rollback)。从而还部分确保了事务的ACID属性.在SQL Server崩溃时,DBA还可以通过事务日志将数据恢复到指定的时间点。当SQL Server运转良好时,多了解一些事务日志的原理和概念显得并不是那么重要。但是,一旦SQL SERVER发生崩溃时,了解事务日志的原理和概念对于快速做出正确的决策来恢复数据显得尤为重要.本系列文章将会从事务日志的概念,原理,SQL Server如何使用日志来确保持久性属性等方面来谈SQL Server的事务日志.

 

事务日志的物理组织构架

事务日志仅仅是记录与其对应数据库上的事务行为和对数据库修改的日志文件.在你新建数据库时,伴随着数据库文件,会有一个默认以ldf为扩展名的事务日志文件. 当然,一个数据库也可以配有多个日志文件,但是在逻辑上,他们可以看成一个.

在SQL Server对于日志文件的管理,是将逻辑上一个ldf文件划分成多个逻辑上的虚拟日志文件(virtual log files,简称VLFs).以便于管理。用个类比方法来看,日志文件(ldf)好比一趟火车,每一节车厢都是一个虚拟日志文件(VLFs):

1

2

 

那为什么SQL Server要把日志文件划分出多个VLFS呢?因为SQL Server通过这种方式使得存储引擎管理事务日志更加有效.并且对于日志空间的重复利用也会更加高效。使用VLF作为收缩数据库的最小单位比使用ldf文件作为最小单位无疑是更加高效的.

VLFS的个数和大小无法通过配置进行设定,而是由SQL Server进行管理.当Create或Alter数据库时,SQL Server通过ldf文件的大小来决定VLFS的大小和数量。在日志文件增长时,SQL Server也会重新规划VLFS的数量.

注意:根据这个原理不难看书,如果设置日志文件的增量过小,则会产生过多的VLFS,也就是日志文件碎片,过多的日志文件碎片会拖累SQL Server性能.

    SQL Server创建数据库时,根据日志文件(ldf)的大小,生成VLF的数量公式如下:

 

ldf文件的大小

VLF的数量

1M到64M

4

64M到1GB

8

大于1GB

16

下面我们来看一个例子:

创建数据库,指定日志大小为65M

3

通过DBCC,我们可以看到,对应的有8个VLFs:

4

再次创建数据库,指定日志初始大小为28M:

5

可以看到,对应的,VLF的数量变为4:

6

而对于日志文件的增长,SQL Server使用了和创建数据库时相同的公式,也就是每次增长比如为2M,则按照公式每次增长4个VLFs.

我们创建一个TestGrow数据库,指定日志文件为2M,此时有4个VLFS:

7

当我们增长2M时,这个2M则是按照公式,再次分配4个VLFs:

8

此时,这时能看到的VLFs数量应该为4+4=8个:

9

由此可以看出,指定合适的日志文件初始大小和增长,是减少日志碎片最关键的部分.

事务日志的逻辑组织构架

当针对数据库对象所做的任何修改保存到数据库之前,相应的日志首先会被记录到日志文件。这个记录会被按照先后顺序记录到日志文件的逻辑末尾,并分配一个全局唯一的日志序列号(log sequence number,简称LSN),这个序列号完全是按照顺序来的,如果日志中两个序列号LSN2>LSN1,则说明LSN2所在LSN1之后发生的.

由此可以看出,将日志文件分为多个文件除了磁盘空间的考虑之外。完全不会像数据那样可以并行访问,所以将日志文件分为多个完全不会有性能上的提升.

    LSN号可以看作是将日志文件和其记录数据之间的纽带.每一条日志不仅有LSN号,还有其对应事务的事务日志:

一个简单的图片示例如下:

10

许多类型的操作都记录在事务日志中。这些操作包括:

  • 每个事务的开始和结束。
  • 每次数据修改(插入、更新或删除)。这包括系统存储过程或数据定义语言 (DDL) 语句对包括系统表在内的任何表所做的更改。
  • 每次分配或释放区和页。
  • 创建或删除表或索引。

 

对于LSN如何在ROLLBACK或者是ROLL FORWARD中以及在备份恢复过程中起作用,会在后续文章中提到

 

 

总结

本篇文章从事务日志的逻辑和物理构架简单介绍了事务日志的构成.这是理解SQL Server如何利用日志保证持久性和数据备份恢复的基础。下一篇文章将会介绍SQL Server在操作中会如何使用到日志文件。