2020年8月4日 By mikel 分类: 数据库, 架构设计

来源: SQL Server常用的性能诊断语句 – 召冠 – 博客园

/*
常规服务器动态管理对象包括:
dm_db_*:数据库和数据库对象
dm_exec_*:执行用户代码和关联的连接
dm_os_*:内存、锁定和时间安排
dm_tran_*:事务和隔离
dm_io_*:网络和磁盘的输入/输出
*/

— 运行下面的 DMV 查询以查看 CPU、计划程序内存和缓冲池信息。
select
cpu_count,
hyperthread_ratio,
scheduler_count,
physical_memory_in_bytes / 1024 / 1024 as physical_memory_mb,
virtual_memory_in_bytes / 1024 / 1024 as virtual_memory_mb,
bpool_committed * 8 / 1024 as bpool_committed_mb,
bpool_commit_target * 8 / 1024 as bpool_target_mb,
bpool_visible * 8 / 1024 as bpool_visible_mb
from sys.dm_os_sys_info

— 高I/O开销的查询 Identifying Most Costly Queries by I/O
SELECT TOP 10
[Average IO] = (total_logical_reads + total_logical_writes) / qs.execution_count
, [Total IO] = (total_logical_reads + total_logical_writes)
, [Execution count] = qs.execution_count
, [Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
(CASE WHEN qs.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
ELSE qs.statement_end_offset END – qs.statement_start_offset)/2)
,[Parent Query] = qt.text
, DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_SQL_text(qs.SQL_handle) as qt
ORDER BY [Average IO] DESC;

— 高CPU开销的查询 Identifying Most Costly Queries by CPU
SELECT TOP 10
[Average CPU used] = total_worker_time / qs.execution_count
, [Total CPU used] = total_worker_time
, [Execution count] = qs.execution_count
, [Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
(CASE WHEN qs.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
ELSE qs.statement_end_offset END – qs.statement_start_offset)/2)
, [Parent Query] = qt.text
, DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_SQL_text(qs.sql_handle) as qt
ORDER BY [Average CPU used] DESC;

— 高开销的缺失索引 Cost of Missing Indexes
SELECT TOP 10
[Total Cost] = ROUND(avg_total_user_cost * avg_user_impact * (user_seeks + user_scans),0)
, avg_user_impact
, TableName = statement
, [EqualityUsage] = equality_columns
, [InequalityUsage] = inequality_columns
, [Include Cloumns] = included_columns
FROM sys.dm_db_missing_index_groups g
INNER JOIN sys.dm_db_missing_index_group_stats s
ON s.group_handle = g.index_group_handle
INNER JOIN sys.dm_db_missing_index_details d
ON d.index_handle = g.index_handle
ORDER BY [Total Cost] DESC;

— 最常执行的查询 Identifying Queries that Execute Most Often
SELECT TOP 10
[Execution count] = execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
(CASE WHEN qs.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
ELSE qs.statement_end_offset END – qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
ORDER BY [Execution count] DESC;

— 重复编译的查询(plan_generation_num 指示该查询已重新编译的次数)
select top 25
sql_text.text,
sql_handle,
plan_generation_num,
execution_count,
dbid,
objectid
from sys.dm_exec_query_stats a
cross apply sys.dm_exec_sql_text(sql_handle) as sql_text
where plan_generation_num > 1
order by plan_generation_num desc

— 服务器等待的原因 SQL Query Records Causes of Wait Times
SELECT TOP 10
[Wait type] = wait_type,
[Wait time (s)] = wait_time_ms / 1000,
[% waiting] = CONVERT(DECIMAL(12,2), wait_time_ms * 100.0
/ SUM(wait_time_ms) OVER())
FROM sys.dm_os_wait_stats
WHERE wait_type NOT LIKE ‘%SLEEP%’
ORDER BY wait_time_ms DESC;

— 读和写 Identifying the Most Reads and Writes
SELECT TOP 10
[Total Reads] = SUM(total_logical_reads)
,[Execution count] = SUM(qs.execution_count)
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
GROUP BY DB_NAME(qt.dbid)
ORDER BY [Total Reads] DESC;

SELECT TOP 10
[Total Writes] = SUM(total_logical_writes)
,[Execution count] = SUM(qs.execution_count)
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
GROUP BY DB_NAME(qt.dbid)
ORDER BY [Total Writes] DESC;

— 运行下面的 DMV 查询以查找 I/O 闩锁等待统计信息。
select wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms, wait_time_ms / waiting_tasks_count
from sys.dm_os_wait_stats
where wait_type like ‘PAGEIOLATCH%’ and waiting_tasks_count > 0
order by wait_type

— 查看SQL阻塞信息
with tmp as (
select * from master..sysprocesses t where t.blocked != 0
union all
select b.* from master..sysprocesses b
join tmp t on b.spid = t.blocked
)
select t.spid, t.blocked, t.status, t.lastwaittype, t.waitresource, t.waittime
, DB_NAME(t.dbid) DbName, t.login_time, t.loginame, t.program_name, dc.text
from (select spid from tmp group by spid) s
join master..sysprocesses t on s.spid = t.spid
cross apply master.sys.dm_exec_sql_text(t.sql_handle) dc

–kill 53;

— 查看所有会话的状态、等待类型及当前正在执行SQL脚本
select t.spid, t.kpid, t.blocked, t.status, t.lastwaittype, t.waitresource, t.waittime
, DB_NAME(t.dbid) DbName, t.last_batch, t.loginame, t.program_name, t.hostname, t.hostprocess , t.cmd, t.stmt_start, t.stmt_end, t.request_id, dc.text
from master.sys.sysprocesses t
outer apply master.sys.dm_exec_sql_text(t.sql_handle) dcwhere t.spid >= 50

select s.spid, s.kpid, s.blocked, s.hostname, s.hostprocess, s.program_name, s.loginame       , s.status, s.lastwaittype, s.waitresource, s.waittime       , t.transaction_id, t.name, t.transaction_begin_time, dc.text   from sys.sysprocesses s      join sys.dm_tran_session_transactions st on s.spid = st.session_id      join sys.dm_tran_active_transactions t on st.transaction_id = t.transaction_id      outer apply master.sys.dm_exec_sql_text(s.sql_handle) dc

—补充,查看所有会话当前持有和申请的锁资源(选择在特定的业务库执行,测试模拟,建议将隔离级别改为可重复读)
set transaction isolation level repeatable read
select  l.request_session_id,          l.resource_type,          l.resource_subtype,          l.request_status,          l.request_mode,          l.resource_description,          db_name(l.resource_database_id) as dbName,          case l.resource_type               when ‘database’ then DB_NAME(l.resource_database_id)              when ‘object’ then object_name(l.resource_associated_entity_id)              else OBJECT_NAME(p.object_id)          end as obj_name,          p.index_id,          l.request_lifetime  from sys.dm_tran_locks l      left join sys.partitions p on l.resource_associated_entity_id = p.hobt_id  order by l.request_session_id, l.resource_type
—查看所有会话的 找到活动事务对应的执行语句
select dc.session_id,
ds.login_name,
ds.login_time,
dc.connect_time,
dc.net_transport,
dc.client_net_address,
ds.host_name,
ds.program_name,
case ds.status when ‘sleeping’ then ‘睡眠 – 当前没有运行任何请求 ‘
when ‘running’ then ‘正在运行 – 当前正在运行一个或多个请求 ‘
when ‘Dormancy’ then ‘休眠 – 会话因连接池而被重置,并且现在处于登录前状态’
when ‘Pre-connected’ then ‘预连接 – 会话在资源调控器分类器中’
end as status ,
ds.cpu_time as cpu_time_ms,
ds.memory_usage*8 as memory_kb,
ds.total_elapsed_time as total_elapsed_time_ms,
case ds.transaction_isolation_level when 0 then ‘未指定’
when 1 then ‘未提交读取’
when 2 then ‘已提交读取’
when 3 then ‘可重复’
when 4 then ‘可序列化’
when 5 then ‘快照’
end ‘会话的事务隔离级别’,
dt.text
from sys.dm_exec_connections dc –执行连接,最近执行的查询信息
cross apply sys.dm_exec_sql_text(dc.most_recent_sql_handle) dt
join sys.dm_exec_sessions ds on dc.session_id=ds.session_id
where ds.login_name= ‘LCGS609999’
–where ds.program_name = ‘.Net SqlClient Data Provider’
ORDER BY dt.text

SQL Server常用的性能诊断语句 – 召冠 – 博客园已关闭评论
2020年8月4日 By mikel 分类: 数据库, 架构设计

来源: 了解SQL Server锁争用:NOLOCK 和 ROWLOCK 的秘密 – Athrun – 博客园

关系型数据库,如SQL Server,使用锁来避免多用户修改数据时的并发冲突。当一组数据被某个用户锁定时,除非第一个用户结束修改并释放锁,否则其他用户就无法修改该组数据。

有些数据库,包括SQL Server,用锁来避免用户检索未递交的修改记录。在这些系统中,如果用户A在修改一组记录,则其他用户只有等用户A修改完毕了,才能检索。

数据库在每个物理层上设置锁:记录行(rows),数据页(pages, 上百万记录行),扩展页(extends, 多个数据页),整个表,甚至整个数据库。有些数据库(如Oracle等)只使用精细的行锁机制,而别的数据库,则使用在页面,扩展页,表和数据库上的较大范围的锁机制。大多数数据库,包括SQL Server,同样支持行锁机制,但是经常使用的还是大范围锁机制。 这主要是因为管理锁需要付出高昂的代价。锁十分复杂而且数量很多,所以如果全都是 行锁的话,将是极为痛苦的:一百万行的数据更新就会轻易消耗巨大的内存,从而根本无法进行管理。

锁争用的描述

那些不仅仅使用行级锁的数据库使用一种称为混和锁(lock escalation)的技术来获取较高的性能。除非很明确知道是针对整个数据表,否则这些数据库的做法是开始使用行级锁, 然后随着修改的数据增多,开始使用大范围的锁机制。

不幸的是,这种混和锁的方法会产生和放大新的问题:死锁。如果两个用户以相反的顺序修改位于不同表的记录,而这两条记录虽然逻辑上不相关, 但是物理上是相邻的,操作就会先引发行锁,然后升级为页面锁。这样, 两个用户都需要对方锁定的东西,就造成了死锁。

例如:

用户A修改表A的一些记录,引发的页面锁不光锁定正在修改的记录,还会有很多其它记录也会被锁定。

用户B修改表B的一些记录,引发的页面锁锁定用户A和其它正在修改的数据。

用户A想修改用户B在表B中锁定(并不一定正在修改的)数据。

用户B想修改或者仅仅想访问用户A在表A中锁定(并不一定正在修改)的数据。

为了解决该问题,数据库会经常去检测是否有死锁存在,如果有,就把其中的一个事务撤销,好让另一个事务能顺利完成。一般来说,都是撤销 那个修改数据量少的事务,这样回滚的开销就比较少。使用行级锁的数据库 很少会有这个问题,因为两个用户同时修改同一条记录的可能性极小,而且由于极其偶然的修改数据的顺序而造成的锁也少。

而且,数据库使用锁超时来避免让用户等待时间过长。查询超时的引入也是为了同样目的。我们可以重新递交那些超时的查询,但是这只会造成数据库 的堵塞。如果经常发生超时,说明用户使用SQL Server的方式有问题。正常 情况是很少会发生超时的。

在服务器负载较高的运行环境下,使用混合锁的SQL Server锁机制,表现不会很好。 原因是锁争用(Lock Contention)。锁争用造成死锁和锁等待问题。在一个多用户系统中,很多用户会同时在修改数据库,还有更多的用户在同时访问数据库,随时会产生锁,用户 也争先恐后地获取锁以确保自己的操作的正确性,死锁频繁发生,这种情形下, 用户的心情可想而知。

确实,如果只有少量用户,SQL Server不会遇到多少麻烦。内部测试和发布的时候,由于用户较少, 也很难发现那些并发问题。但是当激发几百个并发,进行持续不断地INSERT,UPDATE,以及一些 DELETE操作时,如何观察是否有麻烦出现,那时候你就会不得不手忙脚乱地去阅读Oracle的文献。 不过我有一个解决办法,该方法只需要检查你的T-SQL代码,很少的调整和系统测试。用该方法教你进行适当的系统测试过程。

锁争用的解决方法

如果你在今年6月-8月之间访问Streamload.com,你可能会看到诸如“遇到死锁”,“锁超时”, “需要对象”等错误。这些错误都是由于锁争用引起的。在查阅大量文档和讨论后,我了解了这方面的知识,也就是上面所论述的内容,我再次叙述如下:

SQL Server开始是用行级锁的,但是经常会扩大为页面锁和表锁,最终造成死锁。

即使用户没有修改数据,SQL Server在SELECT的时候也会遇到锁。幸运的是,我们可以通过SQL Server 的两个关键字来手工处理:NOLOCK和ROWLOCK。

它们的使用方法如下:

SELECT COUNT(UserID)
FROM Users WITH (NOLOCK)
WHERE Username LIKE ‘foobar’

UPDATE Users WITH (ROWLOCK)
SET Username = ‘fred’ WHERE Username = ‘foobar’

NOLOCK的使用

NOLOCK可以忽略锁,直接从数据库读取数据。这意味着可以避开锁,从而提高性能和扩展性。但同时也意味着代码出错的可能性存在。你可能会读取到运行事务正在处理的无须验证的未递交数据。 这种风险可以量化。

如果是金融方面的代码或者一些非常规的总计(你想绝对保证安全性),你应该小心行事并且不使用这种技术。 但是我认为使用该技术会比你90%应用系统性能要好,当用户(或者是交互代码)发现一个未递交的修改时,使用技术会保证不会像未使用该技术那样引起大麻烦。实际上,你可能发现你的大多数数据很少或者甚至不进行 修改的,这样我们就不会因为这些数据被锁住而浪费大量的时间。

例如,如果你想统计在2000年6月份到8月份之间加入Streamload.com的所有用户,就没有理由去锁住任何记录: 2000年9月1号一到来,这个用户数就是确定的。又例如要列举在Streamload.com的文件列表:这种结果即使 不是100%的正确,也不是大问题。因为你要么不拥有该文件,当然也无所谓你是否能找到它,或者你确实拥有该文件,这种情况下你当然知道你是否修改了该文件,以及该文件是否已经上传完毕了。

但是,如果这些数据的修改,对数据库来说是基础性的修改,或者这些数据对于用户来说,必须是百分之百保证 是修改正确的(例如帐单或者余额数据),那么你不要使用该技术。

ROWLOCK的使用

ROWLOCK告诉SQL Server只使用行级锁。ROWLOCK语法可以使用在SELECT,UPDATE和DELETE语句中,不过 我习惯仅仅在UPDATE和DELETE语句中使用。如果在UPDATE语句中有指定的主键,那么就总是会引发行级锁的。但是当SQL Server对几个这种UPDATE进行批处理时,某些数据正好在同一个页面(page),这种情况在当前情况下 是很有可能发生的,这就象在一个目录中,创建文件需要较长的时间,而同时你又在更新这些文件。当页面锁引发后,事情就开始变得糟糕了。而如果在UPDATE或者DELETE时,没有指定主键,数据库当然认为很多数据会收到影响,那样 就会直接引发页面锁,事情同样变得糟糕。

通过指定使用行级锁,这种情况可以得到避免。但是需要小心的是,如果你错误地使用在过多行上,数据库并不会聪明到自动将行级锁升级到页面锁,服务器也会因为行级锁的开销而消耗大量的内存和CPU,直至无法响应。尤其主要留意的是 企业管理器中”管理/当前活动”(Management/Current Activity)这一项。该项会花较长的时间来载入锁的信息。这些信息 时十分有用的,当你使用行级锁后,你如果在”锁/处理”(Locks/Processes)下看到几百个锁,一点都不奇怪,而恰恰应该庆幸锁超时和死锁的问题减少了。

注意事项

我认为SQL Server倾向于使用NOLOCK关键字,而ROWLOCK关键字由用户根据情况自行决定。你可以仅仅在 SELECT语句中使用NOLOCK,这些SELECT语句场合包括INNER查询,以及在INSERT语句中的SELECT使用,在连接查询下也可以使用,例如:

SELECT COUNT(Users.UserID)
FROM Users WITH (NOLOCK)
JOIN UsersInUserGroups WITH (NOLOCK) ON
Users.UserID = UsersInUserGroups.UserID

NOLOCK 和 ROWLOCK的使用效果

很难去量化在使用NOLOCK和ROWLOCK后,Streamload.com或者你的网站性能到底改善了多少。 不过在使用NOLOCK和ROWLOCK前,Streamload.com的速度很慢,而且经常无法使用,以及很不稳定。使用后,就变得快速、容易访问以及稳定了。两者简直就是天壤之别。这些改变当然无法在 关于锁的文档中很难找到。那些文档会建议你重写你的应用,当表数据被使用,锁产生了(没错,就是这样),然后你应该使用小事务并且以批处理的形式执行(不错,实际经验就是如此),使用低级别的隔离措施 (也没错,NOLOCK就是一个极端的例子),还建议你有限的连接,从而让处理器进行合作(好复杂的描述,而且总觉得怪怪的不像个好点子)。我不知道是否用数据库咨询师会提到本文中的技术(或类似的技术), 但是我只想说的是,Streamload.com的运行状况的确因为该技术得到了改善。如果你遇到了锁争用的问题,也可以试试NOLOCK和ROWLOCK。

申明

是否使用NOLOCK和ROWLOCK,需要自行判断,并谨慎运用。我用该技术的方法是通过查看我的存储过程和即时查询语句,在我自己的理解上来觉得哪里用和如何用。我需要判断如果用NOLOCK 而引起一些返回的不准确,或者ROWLOCK是否会造成太多的锁,这些情况出现时,对于访问者或者使用者来说,是否是可以接受的。在大多数情况下,我认为是没有问题的,但是也许你的代码不适用, 你需要小心对待。你需要创建一些独立的过程,是否加锁,如何加锁,以作为对比。当UPDATE或者 DELETE查询影响到很多数据行时,你在使用PAGELOCK,TABLOCK时也会遇到别的问题。

附:

---------------
 UPDLOCK
  读取表时使用更新锁,而不使用共享锁,并将锁一直保留到语句或事务的结束。UPDLOCK 的优点是允许您读取数据(不阻塞其它事务)并在以后更新数据,同时确保自从上次读取数据后数据没有被更改。
  这是SQLServer2000中对更新锁的说明.
  当我们用UPDLOCK来读取记录时可以对取到的记录加上更新锁,从而加上锁的记录在其它的线程中是不能更改的只能等本线程的事务结束后才能更改,我如下示例:
BEGIN TRANSACTION –开始一个事务
SELECT Qty
FROM myTable WITH (UPDLOCK)
WHERE Id in (1,2,3)
UPDATE myTable SET Qty = Qty – A.Qty
FROM myTable  AS A
INNER JOIN  @_Table AS B ON A.ID = B.ID
COMMIT TRANSACTION –提交事务
  这样在更新时其它的线程或事务在这些语句执行完成前是不能更改ID是1,2,3的记录的.其它的都可以修改和读,1,2,3的只能读,要是修改的话只能等这些语句完成后才能操作.从而保证的数据的修改正确.
了解SQL Server锁争用:NOLOCK 和 ROWLOCK 的秘密 – Athrun – 博客园已关闭评论
2020年8月4日 By mikel 分类: 数据库, 架构设计

来源: SQL Server中的锁 详解 nolock,rowlock,tablock,xlock,paglock – Alfa – 博客园

高手进 锁 nolock,rowlock,tablock,xlock,paglock
锁 nolock,rowlock,tablock,xlock,paglock
请问大哥,在什么情况下用什么样的锁,小弟不太明白。

——解决方案——————–
SQL code
锁定提示 描述
HOLDLOCK 将共享锁保留到事务完成,而不是在相应的表、行或数据页不再需要时就立即释放锁。HOLDLOCK 等同于 SERIALIZABLE。
NOLOCK 不要发出共享锁,并且不要提供排它锁。当此选项生效时,可能会读取未提交的事务或一组在读取中间回滚的页面。有可能发生脏读。仅应用于 SELECT 语句。
PAGLOCK 在通常使用单个表锁的地方采用页锁。
READCOMMITTED 用与运行在提交读隔离级别的事务相同的锁语义执行扫描。默认情况下,SQL Server 2000 在此隔离级别上操作。
READPAST 跳过锁定行。此选项导致事务跳过由其它事务锁定的行(这些行平常会显示在结果集内),而不是阻塞该事务,使其等待其它事务释放在这些行上的锁。READPAST 锁提示仅适用于运行在提交读隔离级别的事务,并且只在行级锁之后读取。仅适用于 SELECT 语句。
READUNCOMMITTED 等同于 NOLOCK。
REPEATABLEREAD 用与运行在可重复读隔离级别的事务相同的锁语义执行扫描。
ROWLOCK 使用行级锁,而不使用粒度更粗的页级锁和表级锁。
SERIALIZABLE 用与运行在可串行读隔离级别的事务相同的锁语义执行扫描。等同于 HOLDLOCK。
TABLOCK 使用表锁代替粒度更细的行级锁或页级锁。在语句结束前,SQL Server 一直持有该锁。但是,如果同时指定 HOLDLOCK,那么在事务结束之前,锁将被一直持有。
TABLOCKX 使用表的排它锁。该锁可以防止其它事务读取或更新表,并在语句或事务结束前一直持有。
UPDLOCK 读取表时使用更新锁,而不使用共享锁,并将锁一直保留到语句或事务的结束。UPDLOCK 的优点是允许您读取数据(不阻塞其它事务)并在以后更新数据,同时确保自从上次读取数据后数据没有被更改。
XLOCK 使用排它锁并一直保持到由语句处理的所有数据上的事务结束时。可以使用 PAGLOCK 或 TABLOCK 指定该锁,这种情况下排它锁适用于适当级别的粒度

——解决方案——————–
SQL code
就启明星提出的在SQL Server中使用加锁的问题,我就以前的经验和收集的一些资料简单的提出我自己的一些看法,不知道对启明星是否有所帮助:
一般而言,下面是个典型的打开数据库的过程。
<%
’游标类型
Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

’加锁类型
Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4
>%

<% set conn = server.createobject(’adodb.connection’) >%
<% set rsmov = server.createobject(’adodb.recordset’) >%
<% conn.open ’soc’, ’’, ’’ >%
<% rsmov.open sqlmov, conn, adopenkeyset, adlockreadonly >%
游标使用时是比较灵活的,它有时用来描述一个记录集,有时又是用来描述当前记录集中某一条记录的指针。游标主要是用来建立一个关系数据库中行/列关系的一种SQL可利用的访问格。与游标有关系的技术术语还有一个叫Bookmark的。如果你选择的游标方式支持Bookmarks。数据库将提供有关记录数目的强大功能。在上面写出的那么多游标方式中,adOpenDynamic是没有太的用处的,虽然它提供实时显示数据库中的记录的所有更新操作的功能,但是因为并不是所有的数据库都支持该游标方式,没有移植性的游标方式对当前错综复杂的数据库来说真是用处不大。在实际的编程中,我相信大家使用得最频繁的是adOpenStatic方式,当然这种方式的缺点是不能够就、实时反应出数据库中内容改变时的状况。如果要想看到数据库被其它用户改变的状况,可使用adOpenKeyse方式(但是它只能够反应出被编辑的改变情况,也就是说不能够反映出新增和删除记录的改变情况。)
其实上面的内容大家一般都可以在微软的技术参考资料中找到,下面来说说在使用这些游标
方式和加锁方式时要注意到的问题。
1。首先要注意到的是这两种方式在混合使用时的问题,就是说你同时设置游标方式和加锁方式。
除非你是在使用Access数据库,一般而言当你混合使用时是并不能够得到你预期想要的游标方式和加锁方式的。例如,如果你同时将游标设置为adOpenStatic方式,而将加锁设置为adLockOptimistic,你将得不到adOpenStatic方式的游标,你这时使用的游标方式将是
adOpenKeyset,也就是说你使用ADO的话,它将返回adOpenKeyset的游标。
2。其次,游标和加锁的混合使用还会导致ADO返回的不是你想要的加锁方式,ADO会改变你的加锁
方式。例如,在默认状态下游标方式是adOpenForwardOnly,在使用这种游标方式的同时如果
你使用的加锁方式为-1(就是让数据源来判断加锁方式)或则adLockReadOnly,那么这种混合方式基本上不支持RecordSet的任何方法,也就是说RecordSet的任何方法将返回False
(你的recordcount,absoultpage,addnew,delete,update等都会返回-1,-1就是表示不支持该属性),但是这时如果你使用的是adOpenForwardOnly游标方式和其它的加锁方式混合,它反而
会支持填加,删除和更新。

————————————————————————–

SELECT 语句中“加锁选项”的功能说明

SQL Server提供了强大而完备的锁机制来帮助实现数据库系统的并发性和高性能。用户既能使用SQL Server的缺省设置也可以在select 语句中使用“加锁选项”来实现预期的效果。 本文介绍了SELECT语句中的各项“加锁选项”以及相应的功能说明。
功能说明:
NOLOCK(不加锁)
此选项被选中时,SQL Server 在读取或修改数据时不加任何锁。 在这种情况下,用户有可能读取到未完成事务(Uncommited Transaction)或回滚(Roll Back)中的数据, 即所谓的“脏数据”。

HOLDLOCK(保持锁)
此选项被选中时,SQL Server 会将此共享锁保持至整个事务结束,而不会在途中释放。

UPDLOCK(修改锁)
此选项被选中时,SQL Server 在读取数据时使用修改锁来代替共享锁,并将此锁保持至整个事务或命令结束。使用此选项能够保证多个进程能同时读取数据但只有该进程能修改数据。

TABLOCK(表锁)
此选项被选中时,SQL Server 将在整个表上置共享锁直至该命令结束。 这个选项保证其他进程只能读取而不能修改数据。

PAGLOCK(页锁)
此选项为默认选项, 当被选中时,SQL Server 使用共享页锁。

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

使用这些选项将使系统忽略原先在SET语句设定的事务隔离级别(Transaction Isolation Level)。 请查阅SQL Server 联机手册获取更多信息。
————————————————————————-

1 如何锁一个表的某一行

A 连接中执行

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

begin tran

select * from tablename with (rowlock) where id=3

waitfor delay ’00:00:05’

commit tran

B连接中如果执行

update tablename set colname=’10’ where id=3 –则要等待5秒

update tablename set colname=’10’ where id <>3 –可立即执行

2 锁定数据库的一个表

SELECT * FROM table WITH (HOLDLOCK)

注意: 锁定数据库的一个表的区别

SELECT * FROM table WITH (HOLDLOCK)
其他事务可以读取表,但不能更新删除

SELECT * FROM table WITH (TABLOCKX)
其他事务不能读取表,更新和删除

select * from table with (..)

SELECT 语句中“加锁选项”的功能说明
SQL Server提供了强大而完备的锁机制来帮助实现数据库系统的并发性和高性能。用户既能使用SQL Server的缺省设置也可以在select 语句中使用“加锁选项”来实现预期的效果。 本文介绍了SELECT语句中的各项“加锁选项”以及相应的功能说明。
功能说明:
NOLOCK(不加锁)
此选项被选中时,SQL Server 在读取或修改数据时不加任何锁。 在这种情况下,用户有可能读取到未完成事务(Uncommited Transaction)或回滚(Roll Back)中的数据, 即所谓的“脏数据”。

HOLDLOCK(保持锁)
此选项被选中时,SQL Server 会将此共享锁保持至整个事务结束,而不会在途中释放。

UPDLOCK(修改锁)
此选项被选中时,SQL Server 在读取数据时使用修改锁来代替共享锁,并将此锁保持至整个事务或命令结束。使用此选项能够保证多个进程能同时读取数据但只有该进程能修改数据。

TABLOCK(表锁)
此选项被选中时,SQL Server 将在整个表上置共享锁直至该命令结束。 这个选项保证其他进程只能读取而不能修改数据。

PAGLOCK(页锁)
此选项为默认选项, 当被选中时,SQL Server 使用共享页锁。

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

使用这些选项将使系统忽略原先在SET语句设定的事务隔离级别(Transaction Isolation Level)。 请查阅SQL Server 联机手册获取更多信息。

————————————————

什幺是事务
事务(Transaction)是并发控制的基本单位。所谓事务,它是一个操作序列,这些操作要幺都执行,要幺都不执行,它是一个不可分割的工作单位。例如,银行转帐工作:从一个帐号扣款并使另一个帐号增款,这两个操作要幺都执行,要幺都不执行。所以,应该把他们看成一个事务。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

数据一致性问题
多用户并发存取同一数据将会导致以下的数据不一致性问题。
• 丢失修改( Lost Update)
在下表中,T1、T2、T3和T4表示顺序的时间。
用户T 1T 2T 3T 4
Ax = 40X = x-30
BX = 40X = x-20

假设用户A和B都读取x ( x = 40 ) ,然后分别把x减少30和20。用户A在t3把改后的x ( x = 10 )写入数据库。随后,用户B在t4把改后的x ( x = 20 )写入数据库。于是,对用户A而言,他的修改在t4
处丢失了。
• 脏读数据( Dirty Read)
请看下表,
用户T1T2T3T4
Ax = 40X = x + 30X = x – 30rollback
BX = 70X = x-20
用户A在t2把x增加30(尚没写入数据库),用户B在t3由数据缓存读出x = 70。但用户A在t4时撤消(Undo)了对x的修改,数据库中仍维持x = 40。但用户B已把改变的数据( x = 70)取走。
• 不能重复读(Non-Repeatable Read)
用户T1T2T3T4T5T6
AX=40Y=30 X+Y=70Z=30 X+Y+Z=100
Bx=40X=X+20CommitX=x-20
用户A、用户B分别读取x = 40后,在t 3用户A取出y = 30并计算x + y = 70。在t4时用户B把x增加20,并于t 5把x ( x = 60 )写入数据库。在t6时,用户A取出z ( z = 30 )并继续计算x + y + z = 100。但如果用户A为进行核算而把x、y、x重读一次再进行计算,却出现x + y + z = 120!(x已增加20)。

如何标识一个事务
在SQL Server中,通常事务是指以BEGIN TRAN开始,到ROLLBACK或一个相匹配的COMMIT之间的所有语句序列。ROLLBACK表示要撤消( U n d o)该事务已做的一切操作,回退到事务开始的状态。COMMIT表示提交事务中的一切操作,使得对数据库的改变生效。
在SQL Server中,对事务的管理包含三个方面:
• 事务控制语句:它使程序员能指明把一系列操作( Transact – SQL命令)作为一个工作单
位来处理。
• 锁机制( Locking):封锁正被一个事务修改的数据,防止其它用户访问到“不一致”的数据。
• 事务日志( Transaction Log):使事务具有可恢复性。

SQL Server的锁机制
所谓封锁,就是一个事务可向系统提出请求,对被操作的数据加锁( Lock )。其它事务必须等到此事务解锁( Unlock)之后才能访问该数据。从而,在多个用户并发访问数据库时,确保不互相干扰。可锁定的单位是:行、页、表、盘区和数据库。
1. 锁的类型
SQL Server支持三种基本的封锁类型:共享( S)锁,排它(X)锁和更新(U)锁。封锁的基本粒度为行。
1) 共享(S)锁:用于读操作。
• 多个事务可封锁一个共享单位的数据。
• 任何事务都不能修改加S锁的数据。

通常是加S锁的数据被读取完毕,S锁立即被释放。
2) 独占(X)锁:用于写操作。
• 仅允许一个事务封锁此共享数据。
• 其它任何事务必须等到X锁被释放才能对该数据进行访问。
• X锁一直到事务结束才能被释放。
3) 更新(U)锁。
• 用来预定要对此页施加X锁,它允许其它事务读,但不允许再施加U

锁或X锁。
• 当被读取数据页将要被更新时,则升级为X锁。
• U锁一直到事务结束时才能被释放。
2. 三种锁的兼容性
如下表简单描述了三种锁的兼容性:
通常,读操作(SELECT)获得共享锁,写操作( INSERT、DELETE)获得独占锁;而更新操作可分解为一个有更新意图的读和一个写操作,故先获得更新锁,然后再升级为独占锁。
执行的命令获得锁其它进程可以查询?其它进程可以修改?
Select title_id from titlesSYesNo
delete titles where price>25XNoNo
insert titles values( …)XNoNo
update titles set type=“general”UYesNo
where type=“business”然后XNONo

使用索引降低锁并发性
我们为什幺要讨论锁机制?如果用户操作数据时尽可能锁定最少的数据,这样处理过程,就不会等待被锁住的数据解锁,从而可以潜在地提高SQL Server的性能。如果有200个用户打算修改不同顾客的数据,仅对存储单个顾客信息的单一行进行加锁要比锁住整个表好得多。那幺,用户如何只锁定行而不是表呢?当然是使用索引了。正如前面所提到的,对存有要修改数据的字段使用索引可以提高性能,因为索引能直接找到数据所在的页面,而不是搜索所有的数据页面去找到所需的行。如果用户直接找到表中对应的行并进行更新操作,只需锁定该行即可,而不是锁定多个页面或者整个表。性能的提高不仅仅是因为在修改时读取的页面较少,而且锁定较少的页面潜在地避免了一个用户在修改数据完成之前其它用户一直等待解锁的情况。

事务的隔离级别
ANSI标准为SQL事务定义了4个隔离级别(isolation level),隔离级别越高,出现数据不一致性的可能性就越小(并发度也就越低)。较高的级别中包含了较低级别中所规定了的限制。
• 隔离级别0:防止“丢失修改”,允许脏读。
• 隔离级别1:防止脏读。允许读已提交的数据。
• 隔离级别2:防止“不可重复读”。
• 隔离级别3:“可串行化”(serializable)。其含义为,某组并行事务的一种交叉调度产生的结果和这些事务的某一串行调度的结果相同(可避免破坏数据一致性)。SQL Server支持四种隔离级别,级别1为缺省隔离级别,表中没有隔离级别2, 请参考表:
SQL Server支持的隔离级别封锁方式数据一致性保证
X锁施加于被修改的页S锁施加于被读取的页防止丢失修改防止读脏数据可以重复读取
级别0封锁到事务结束是
级别1(缺省)封锁到事务结束读后立即释放是是
级别3封锁到事务结束封锁到事务结束是是是
在SQL Server也指定级别2,但级别3已包含级别2。ANSI-92 SQL中要求把级别3作为所有事务的缺省隔离级别。
SQL Server用holdlock选项加强S锁的限制,实现隔离级别3。SQL Server的缺省隔离级别为级别1,共享读锁(S锁)是在该页被读完后立即释放。在select语句中加holdlock选项,则可使S锁一直保持到事务结束才释放。她符合了ANSI隔离级别3的标准─“可串行化”。

下面这个例子中,在同一事务中对avg ( advance )要读取两次,且要求他们取值不变─“可重复读”,为此要使用选项holdlock。
BEGIN tran
DECLARE @avg-adv money
SELECT @avg-adv = avg(advance)
FROM titles holdlock
WHERE type = “business“
if @avg-adv > 5000
SELECT title from titles
WHERE type=“business“ and advance >@avg_adv
COMMIT tran
在SQL Server中设定事务隔离级别的方法有三种:

• 会话层设定
语法如下:
SET TRANSACTION ISOLATION LEVEL
{
READ COMMITTED
| READ UNCOMMITTED
| REPEATABLE READ
| SERIALIZABLE
}
系统提供的系统存储过程将在级别1下执行,它不受会话层设定的影响。
• 语法层设定
在SELECT、DECLARE cursor及read text语句中增加选项。比如:
SELECT…at isolation{0|read uncommitted}
注意:语法层的设定将替代会话层的设定。
• 利用关键词设定
─在SELECT语句中,加选项holdlock则设定级别3
─在SELECT语句中,加noholdlock则设定级别0

如下程序清单中所列的脚本实例在authors表上持有一个共享锁,它将用户检查服务器当前活动的时间推迟两分钟。
程序清单测试事务隔离等级
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
BEGIN TRAN
SELECT *
FROM authors
WHERE au_lname = ’Green’
WAITFOR DELAY ’00:02:00’
ROLLBACK TRAN
GO
Activity Legend(活动图标)表明:当SQL Server检索数据时会去掉页面表意向锁。Current Activity窗口(见图3 – 3 )显示共享锁一直被保持直到事务完成为止(也就是说,直到WAITFOR和ROLLBACK TRAN语句完成)。
使用锁定优化程序提示
让我们再深入考察程序清单的实例。通过改变优化程序提示,用户可以令SQL Server在authors表上设置一个独占表锁(如程序所示)。
BEGIN TRAN
SELECT *
FROM authors (tablockx)
WHERE au_lname = ’Green’
WAITFOR DELAY ’00:02:00’
ROLLBACK TRAN
GO

SQL Server中的锁 详解 nolock,rowlock,tablock,xlock,paglock – Alfa – 博客园已关闭评论
2020年8月2日 By mikel 分类: C#, Python

来源: 抖音抓包 – 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/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
抖音抓包 – maanshancss – 博客园已关闭评论
2020年8月2日 By mikel 分类: Python

来源: 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值,希望大佬们可以提出建议让我把这个爬虫的功能再完善!感谢各位!

Python爬虫—爬取抖音短视频 – merlin& – 博客园已关闭评论
2020年8月2日 By mikel 分类: C#, 数据库, 架构设计

来源: 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; }
    }
复制代码

执行结果如下:

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

@陈卧龙的博客

C# 集合-并发处理-锁OR线程 – 天才卧龙 – 博客园已关闭评论
2020年8月2日 By mikel 分类: 数据库, 架构设计

来源: 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-乐观锁,悲观锁之于并发 – 天才卧龙 – 博客园已关闭评论
2020年8月2日 By mikel 分类: 数据库, 架构设计

来源: 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中灾难时备份结尾日志(Tail of log)的两种方法 – CareySon – 博客园已关闭评论
2020年8月2日 By mikel 分类: 数据库, 架构设计

来源: 浅谈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 – 博客园已关闭评论
2020年8月2日 By mikel 分类: 数据库, 架构设计

来源: 浅谈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 Server中的事务日志(三)—-在简单恢复模式下日志的角色 – CareySon – 博客园已关闭评论
备案信息冀ICP 0007948