今天突然关注到这个问题,从网上看了非常多,受益良多。记录下来,以后回想~
之前在工作中就遇到过这样的情况。两个用户同一时候操作一条记录,A用户查询某条记录,B用户把这条记录删除,A用户将查询的某条记录的某些值保存到其它的表里。这个bug也是困扰了好久,由于A用户的这种方法特别复杂,运行的时间比較长,所以这个问题出现的概率还非常高的呢。后来的解决方法是,A用户在最后保存前,再查一下这条记录。是从代码逻辑方面解决的这个问题,确实好了非常多,但始终认为是治标不治本。
今天看完以后认为有更好的解决方法的呢。
part1:
大并发大数据量请求通常会分为几种情况:
1.大量的用户同一时候对系统的不同功能页面进行查找,更新操作
2.大量的用户同一时候对系统的同一个页面,同一个表的大数据量进行查询操作
3.大量的用户同一时候对系统的同一个页面。同一个表进行更新操作
对于第一种情况一般处理方法例如以下:
一。对server层面的处理
1. 调整IIS 7应用程序池队列长度
由原来的默认1000改为65535。
IIS Manager > ApplicationPools > Advanced Settings
Queue Length : 65535
2. 调整IIS 7的appConcurrentRequestLimit设置
由原来的默认5000改为100000。
c:windowssystem32inetsrvappcmd.exe set config /section:serverRuntime /appConcurrentRequestLimit:100000
在%systemroot%System32inetsrvconfigapplicationHost.config中能够查看到该设置:
- <serverRuntime appConcurrentRequestLimit="100000" />
3. 调整machine.config中的processModel>requestQueueLimit的设置
由原来的默认5000改为100000。
- <configuration>
- <system.web>
- <processModel requestQueueLimit="100000"/>
4. 改动注冊表,调整IIS 7支持的同一时候TCPIP连接数
由原来的默认5000改为100000。
reg add HKLMSystemCurrentControlSetServicesHTTPParameteris /v MaxConnections /t REG_DWORD /d 100000
完毕上述4个设置,就基本能够支持10万个同一时候请求。假设訪问量达到10万以上。就能够考虑将程序和数据库按功能模块划分部署到多个server分担訪问压力。另外能够考虑软硬件负载均衡。硬件负载均衡能够直接通过智能交换机实现,处理能力强。并且与系统无关。可是价格贵,配置困难。不能区分实习系统与应状态。
所以硬件负载均衡适用于一大堆设备,大訪问量,简单应用。软件负载均衡是基于系统与应用的,能过更好地依据系统与应用的状况来分配负载。
性价比高。
PCL负载均衡软件,Linux下的LVS软件。
二。
对数据库层面的处理
当两个用户同一时候訪问一个页面,一个用户可能更新的是还有一个用户已经删除的记录。
或者,在一个用户载入页面跟他点击删除button之间的时间里。还有一个用户改动了这条记录的内容。所以须要考虑数据库锁的问题
有以下三中并发控制策略可供选择:
Ø 什么都不做 –假设并发用户改动的是同一条记录,让最后提交的结果生效(默认的行为)
Ø 开放式并发(Optimistic Concurrency) - 假定并发冲突不过偶尔发生,绝大多数的时候并不会出现; 那么,当发生一个冲突时,只简单的告知用户,他所作的更改不能保存,由于别的用户已经改动了同一条记录
Ø 保守式并发(Pessimistic Concurrency) – 假定并发冲突常常发生,而且用户不能容忍被告知自己的改动不能保存是因为别人的并发行为;那么,当一个用户開始编辑一条记录,锁定该记录。从而防止其它用户编辑或删除该记录,直到他完毕并提交自己的更改
当多个用户试图同一时候改动数据时,须要建立控制机制来防止一个用户的改动对同一时候操作的其它用户所作的改动产生不利的影响。处理这样的情况的系统叫做“并发控制”。
并发控制的类型
通常,管理数据库中的并发有三种常见的方法:
- 保守式并发控制 - 在从获取记录直到记录在数据库中更新的这段时间内,该行对用户不可用。
- 开放式并发控制 - 仅仅有当实际更新数据时,该行才对其它用户不可用。更新将在数据库中检查该行并确定是否进行了不论什么更改。假设试图更新已更改的记录。则将导致并发冲突。
- 最后的更新生效 - 仅仅有当实际更新数据时。该行才对其它用户不可用。可是,不会将更新与初始记录进行比較。而仅仅是写出记录,这可能就改写了自上次刷新记录后其它用户所进行的更改。
保守式并发
保守式并发通经常使用于两个目的。第一,在某些情况下,存在对同样记录的大量争用。
在数据上放置锁所费的成本小于发生并发冲突时回滚更改所费的成本。
在事务过程中不宜更改记录的情况下,保守式并发也非常实用。库存应用程序便是一个非常好的演示样例。假定有一个公司代表正在为一名潜在的客户检查库存。您通常要锁定记录,直到生成订单为止,这一般会将该项标记为“已订购”状态并将其从可用库存中移除。假设未生成订单,则将释放该锁,以便其它检查库存的用户得到准确的可用库存计数。
可是,在断开的结构中无法进行保守式并发控制。连接打开的时间仅仅够读取数据或更新数据。因此不能长时间地保持锁。
此外,长时间保留锁的应用程序将无法进行伸缩。
开放式并发
在开放式并发中。仅仅有在訪问数据库时才设置并保持锁。这些锁将防止其它用户在同一时间更新记录。除了进行更新这一确切的时刻之外,数据始终可用。有关很多其它信息。请參见开放式并发。
当试图更新时,已更改行的初始版本号将与数据库中的现有行进行比較。
假设两者不同,更新将失败。并引发并发错误。
这时。将由您使用所创建的业务逻辑来协调这两行。
最后的更新生效
当使用“最后的更新生效”时。不会对初始数据进行检查。而仅仅是将更新写入数据库。非常明显,可能会发生下面情况:
- 用户 A 从数据库获取一项记录。
- 用户 B 从数据库获取同样的记录。对其进行改动,然后将更新后的记录写回数据库。
- 用户 A 改动“旧”记录并将其写回数据库。
在上述情况中。用户 A 永远也不会看到用户 B 作出的更改。假设您计划使用并发控制的“最后的更新生效”方法,则要确保这样的情况是能够接受的。
ADO.NET 和 Visual Studio .NET 中的并发控制
由于数据结构基于断开的数据。所以 ADO.NET 和 Visual Studio .NET 使用开放式并发。因此。您须要加入业务逻辑,以利用开放式并发解决这个问题。
假设您选择使用开放式并发,则能够通过两种常规的方法来确定是否已发生更改:版本号方法(实际版本号号或日期时间戳)和保存全部值方法。
版本方法
在版本方法中。要更新的记录必须具有一个包括日期时间戳或版本的列。当读取该记录时,日期时间戳或版本将保存在client。然后。将对该值进行部分更新。
处理并发的一种方法是仅当 WHERE 子句中的值与记录上的值匹配时才进行更新。该方法的 SQL 表示形式为:
UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2 WHERE DateTimeStamp = @origDateTimeStamp
或者,能够使用版本进行比較:
UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2 WHERE RowVersion = @origRowVersionValue
假设日期时间戳或版本匹配,则表明数据存储区中的记录未被更改。而且能够安全地使用数据集中的新值对该记录进行更新。假设不匹配,则将返回错误。您能够编写代码,在 Visual Studio .NET 中实现这样的形式的并发检查。您还必须编写代码来响应不论什么更新冲突。
为了确保日期时间戳或版本的准确性,您须要在表上设置触发器。以便在发生对行的更改时,对日期时间戳或版本进行更新。
保存全部值方法
使用日期时间戳或版本号号的替代方法是在读取记录时获取全部字段的副本。ADO.NET 中的 DataSet 对象维护每一个改动记录的两个版本号:初始版本号(最初从数据源中读取的版本号)和改动版本号(表示用户更新)。当试图将记录写回数据源时,数据行中的初始值将与数据源中的记录进行比較。假设它们匹配。则表明数据库记录在被读取后尚未经过更改。
在这样的情况下,数据集中已更改的值将成功地写入数据库。
对于数据适配器的四个命令(DELETE、INSERT、SELECT 和 UPDATE)来说,每一个命令都有一个參数集合。每一个命令都实用于初始值和当前值(或改动值)的參数。
对于另外一种情况的处理:
由于是大并发请求,也能採用第一种情况的处理方法,另外由于是对大数据量进行检索。所以须要考虑查询效率的问题
1.对表按查询条件建立索引
2.对查询语句进行优化
3.能够考虑对查询数据使用缓存
对于第三种情况的处理:
也能採用第一种情况的处理方法,另外由于是对同一个表进行更新操作,能够考虑使用以下的处理方法:
1.先将数据保存到缓存中,当数据达到一定的数量后,再更新到数据库中
2.将表按索引划分(分表,分区),如:对于一个存储全国人民信息的表。这个数据量是非常大的,假设按省划分为多个表,在将全国的人民信息按省存储到对应的表中,然后依据省份对对应的并进行查询和更新,这样大并发和大数据量的问题就会减小非常多
part2:
一旦已经读取数据,便马上释放资源上的共享 (S) 锁。除非将事务隔离级别设置为可反复读或更高级别。或者在事务生存周期内用锁定提示保留共享 (S) 锁。
第二个事务试图获取排它 (X) 锁以进行更新。
由于两个事务都要转换为排它 (X) 锁,而且每一个事务都等待还有一个事务释放共享模式锁,因此发生死锁。
一次仅仅有一个事务能够获得资源的更新 (U) 锁。假设事务改动资源,则更新 (U) 锁转换为排它 (X) 锁。
否则,锁转换为共享锁。
比如。放置在表级的共享意向锁表示事务打算在表中的页或行上放置共享 (S) 锁。
在表级设置意向锁可防止还有一个事务随后在包括那一页的表上获取排它 (X) 锁。意向锁能够提高性能,由于 SQL Server 仅在表级检查意向锁来确定事务能否够安全地获取该表上的锁。而无须检查表中的每行或每页上的锁以确定事务能否够锁定整个表。
等到SQL Server确定要进行更新数据操作时,他会自己主动将更新锁换为独占锁,当对象上有其它锁存在时,无法对其加更新锁。
当须要滚动锁时,直到下一次提取或关闭游标(以先发生者为准)时才释放滚动锁。
可是。假设指定 HOLDLOCK,则直到事务结束才释放滚动锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
1
怎样锁一个表的某一行 SET
TRANSACTION
ISOLATION
LEVEL
READ
UNCOMMITTED SELECT
* FROM
table
ROWLOCK WHERE
id = 1 2
锁定数据库的一个表 SELECT
* FROM
table
WITH
(HOLDLOCK) 加锁语句: sybase: update
表 set
col1=col1 where
1=0 ; MSSQL: select
col1 from
表 (tablockx) where
1=0 ; oracle: LOCK
TABLE
表 IN
EXCLUSIVE MODE ; |
1
2
3
4
5
6
7
8
9
10
11
12
|
在第一个连接中运行下面语句 begin
tran update
table1 set
A=’aa’ where
B=’b2′ waitfor
delay ’00:00:30′ –等待30秒 commit
tran 在第二个连接中运行下面语句 begin
tran select
* from
table1 where
B=’b2′ commit
tran |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
在第一个连接中运行下面语句 begin
tran select
* from
table1 holdlock -holdlock人为加锁 where
B=’b2′ waitfor
delay ’00:00:30′ –等待30秒 commit
tran 在第二个连接中运行下面语句 begin
tran select
A,C from
table1 where
B=’b2′ update
table1 set
A=’aa’ where
B=’b2′ commit
tran |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
增设table2(D,E) D
E d1
e1 d2
e2 在第一个连接中运行下面语句 begin
tran update
table1 set
A=’aa’ where
B=’b2′ waitfor
delay ’00:00:30′ update
table2 set
D=’d5′ where
E=’e1′ commit
tran 在第二个连接中运行下面语句 begin
tran update
table2 set
D=’d5′ where
E=’e1′ waitfor
delay ’00:00:10′ update
table1 set
A=’aa’ where
B=’b2′ commit
tran |