欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

DB2和 Oracle的并发控制(锁)的比较

程序员文章站 2023-12-03 12:34:40
1 引言 在关系数据库(db2,oracle,sybase,informix和sql server)最小的恢复和交易单位为一个事务(transactio...
1 引言
在关系数据库(db2,oracle,sybase,informix和sql server)最小的恢复和交易单位为一个事务(transactions),事务具有acid(原子性,一致性,隔离性和永久性)特征。关系数据库为了确保并发用户在存取同一数据库对象时的正确性(即无丢失更新、可重复读、不读"脏"数据,无"幻像"读),数据库中引入了并发(锁)机制。基本的锁类型有两种:排它锁(exclusive locks记为x锁)和共享锁(share locks记为s锁)。
排它锁:若事务t对数据d加x锁,则其它任何事务都不能再对d加任何类型的锁,直至t释放d上的x锁;一般要求在修改数据前要向该数据加排它锁,所以排它锁又称为写锁。
共享锁:若事务t对数据d加s锁,则其它事务只能对d加s锁,而不能加x锁,直至t释放d上的s锁;一般要求在读取数据前要向该数据加共享锁,所以共享锁又称为读锁。
2 db2 多粒度*机制介绍
2.1 锁的对象
db2支持对表空间、表、行和索引加锁(大型机上的数据库还可以支持对数据页加锁)来保证数据库的并发完整性。不过在考虑用户应用程序的并发性的问题上,通常并不检查用于表空间和索引的锁。该类问题分析的焦点在于表锁和行锁。
2.2 锁的策略
db2可以只对表进行加锁,也可以对表和表中的行进行加锁。如果只对表进行加锁,则表中所有的行都受到同等程度的影响。如果加锁的范围针对于表及下属的行,则在对表加锁后,相应的数据行上还要加锁。究竟应用程序是对表加行锁还是同时加表锁和行锁,是由应用程序执行的命令和系统的隔离级别确定。
2.2.1 db2表锁的模式
db2在表一级加锁可以使用以下加锁方式:

表一:db2数据库表锁的模式

下面对几种表锁的模式进一步加以阐述:
is、ix、six方式用于表一级并需要行锁配合,他们可以阻止其他应用程序对该表加上排它锁。
•        如果一个应用程序获得某表的is锁,该应用程序可获得某一行上的s锁,用于只读操作,同时其他应用程序也可以读取该行,或是对表中的其他行进行更改。 
•        如果一个应用程序获得某表的ix锁,该应用程序可获得某一行上的x锁,用于更改操作,同时其他应用程序可以读取或更改表中的其他行。 
•        如果一个应用程序获得某表的six锁,该应用程序可以获得某一行上的x锁,用于更改操作,同时其他应用程序只能对表中其他行进行只读操作。 
s、u、x和z方式用于表一级,但并不需要行锁配合,是比较严格的表加锁策略。
•        如果一个应用程序得到某表的s锁。该应用程序可以读表中的任何数据。同时它允许其他应用程序获得该表上的只读请求锁。如果有应用程序需要更改读该表上的数据,必须等s锁被释放。 
•        如果一个应用程序得到某表的u锁,该应用程序可以读表中的任何数据,并最终可以通过获得表上的x锁来得到对表中任何数据的修改权。其他应用程序只能读取该表中的数据。u锁与s锁的区别主要在于更改的意图上。u锁的设计主要是为了避免两个应用程序在拥有s锁的情况下同时申请x锁而造成死锁的。 
•        如果一个应用程序得到某表上的x锁,该应用程序可以读或修改表中的任何数据。其他应用程序不能对该表进行读或者更改操作。 
•        如果一个应用程序得到某表上的z锁,该应用程序可以读或修改表中的任何数据。其他应用程序,包括未提交读程序都不能对该表进行读或者更改操作。 
in锁用于表上以允许未提交读这一概念。
2.2.2 db2行锁的模式
除了表锁之外,db2还支持以下几种方式的行锁。

表二:db2数据库行锁的模式

2.2.3 db2锁的兼容性

表三:db2数据库表锁的相容矩阵


表四:db2数据库行锁的相容矩阵

下表是笔者总结了db2中各sql语句产生表锁的情况(假设缺省的隔离级别为cs):


2.3 db2锁的升级
每个锁在内存中都需要一定的内存空间,为了减少锁需要的内存开销,db2提供了锁升级的功能。锁升级是通过对表加上非意图性的表锁,同时释放行锁来减少锁的数目,从而达到减少锁需要的内存开销的目的。锁升级是由数据库管理器自动完成的,有两个数据库的配置参数直接影响锁升级的处理:
locklist--在一个数据库全局内存中用于锁存储的内存。单位为页(4k)。
maxlocks--一个应用程序允许得到的锁占用的内存所占locklist大小的百分比。
锁升级会在这两种情况下被触发:
•        某个应用程序请求的锁所占用的内存空间超出了maxlocks与locklist的乘积大小。这时,数据库管理器将试图通过为提出锁请求的应用程序申请表锁,并释放行锁来节省空间。 
•        在一个数据库中已被加上的全部锁所占的内存空间超出了locklist定义的大小。这时,数据库管理器也将试图通过为提出锁请求的应用程序申请表锁,并释放行锁来节省空间。 
•        锁升级虽然会降低oltp应用程序的并发性能,但是锁升级后会释放锁占有内存并增大可用的锁的内存空间。 
锁升级是有可能会失败的,比如,现在一个应用程序已经在一个表上加有ix锁,表中的某些行上加有x锁,另一个应用程序又来请求表上的is锁,以及很多行上的s锁,由于申请的锁数目过多引起锁的升级。数据库管理器试图为该应用程序申请表上的s锁来减少所需要的锁的数目,但s锁与表上原有的ix锁冲突,锁升级不能成功。
如果锁升级失败,引起锁升级的应用程序将接到一个-912的sqlcode。在锁升级失败后,dba应该考虑增加locklist的大小或者增大maxlocks的百分比。同时对编程人员来说可以在程序里对发生锁升级后程序回滚后重新提交事务(例如:if sqlca.sqlcode=-912 then rollback and retry等)。
3 oracle 多粒度锁机制介绍
根据保护对象的不同,oracle数据库锁可以分为以下几大类:
(1) dml lock(data locks,数据锁):用于保护数据的完整性;
(2) ddl lock(dictionary locks,字典锁):用于保护数据库对象的结构(例如表、视图、索引的结构定义);
(3) internal locks 和latches(内部锁与闩):保护内部数据库结构;
(4) distributed locks(分布式锁):用于ops(并行服务器)中;
(5) pcm locks(并行高速缓存管理锁):用于ops(并行服务器)中。
在oracle中最主要的锁是dml(也可称为data locks,数据锁)锁。从*粒度(*对象的大小)的角度看,oracle dml锁共有两个层次,即行级锁和表级锁。
3.1 oracle的tx锁(行级锁、事务锁)
许多对oracle不太了解的技术人员可能会以为每一个tx锁代表一条被*的数据行,其实不然。tx的本义是transaction(事务),当一个事务第一次执行数据更改(insert、update、delete)或使用select… for update语句进行查询时,它即获得一个tx(事务)锁,直至该事务结束(执行commit或rollback操作)时,该锁才被释放。所以,一个tx锁,可以对应多个被该事务锁定的数据行(在我们用的时候多是启动一个事务,然后select… for update nowait)。
在oracle的每行数据上,都有一个标志位来表示该行数据是否被锁定。oracle不像db2那样,建立一个链表来维护每一行被加锁的数据,这样就大大减小了行级锁的维护开销,也在很大程度上避免了类似db2使用行级锁时经常发生的锁数量不够而进行锁升级的情况。数据行上的锁标志一旦被置位,就表明该行数据被加x锁,oracle在数据行上没有s锁。
3.2 tm锁(表级锁)
3.2.1 意向锁的引出
表是由行组成的,当我们向某个表加锁时,一方面需要检查该锁的申请是否与原有的表级锁相容;另一方面,还要检查该锁是否与表中的每一行上的锁相容。比如一个事务要在一个表上加s锁,如果表中的一行已被另外的事务加了x锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,可以在表级引入新的锁类型来表示其所属行的加锁情况,这就引出了"意向锁"的概念。
意向锁的含义是如果对一个结点加意向锁,则说明该结点的下层结点正在被加锁;对任一结点加锁时,必须先对它的上层结点加意向锁。如:对表中的任一行加锁时,必须先对它所在的表加意向锁,然后再对该行加锁。这样一来,事务对表加锁时,就不再需要检查表中每行记录的锁标志位了,系统效率得以大大提高。
3.2.2 意向锁的类型
由两种基本的锁类型(s锁、x锁),可以自然地派生出两种意向锁:
意向共享锁(intent share lock,简称is锁):如果要对一个数据库对象加s锁,首先要对其上级结点加is锁,表示它的后裔结点拟(意向)加s锁;
意向排它锁(intent exclusive lock,简称ix锁):如果要对一个数据库对象加x锁,首先要对其上级结点加ix锁,表示它的后裔结点拟(意向)加x锁。
另外,基本的锁类型(s、x)与意向锁类型(is、ix)之间还可以组合出新的锁类型,理论上可以组合出4种,即:s+is,s+ix,x+is,x+ix,但稍加分析不难看出,实际上只有s+ix有新的意义,其它三种组合都没有使锁的强度得到提高(即:s+is=s,x+is=x,x+ix=x,这里的"="指锁的强度相同)。所谓锁的强度是指对其它锁的排斥程度。
这样我们又可以引入一种新的锁的类型:
共享意向排它锁(shared intent exclusive lock,简称six锁):如果对一个数据库对象加six锁,表示对它加s锁,再加ix锁,即six=s+ix。例如:事务对某个表加six锁,则表示该事务要读整个表(所以要对该表加s锁),同时会更新个别行(所以要对该表加ix锁)。
这样数据库对象上所加的锁类型就可能有5种:即s、x、is、ix、six。
具有意向锁的多粒度*方法中任意事务t要对一个数据库对象加锁,必须先对它的上层结点加意向锁。申请*时应按自上而下的次序进行;释放*时则应按自下而上的次序进行;具有意向锁的多粒度*方法提高了系统的并发度,减少了加锁和解锁的开销。
3.3 oracle的tm锁(表级锁)
oracle的dml锁(数据锁)正是采用了上面提到的多粒度*方法,其行级锁虽然只有一种(即x锁),但其tm锁(表级锁)类型共有5种,分别称为共享锁(s锁)、排它锁(x锁)、行级共享锁(rs锁)、行级排它锁(rx锁)、共享行级排它锁(srx锁),与上面提到的s、x、is、ix、six相对应。需要注意的是,由于oracle在行级只提供x锁,所以与rs锁(通过select … for update语句获得)对应的行级锁也是x锁(但是该行数据实际上还没有被修改),这与理论上的is锁是有区别的。锁的兼容性是指当一个应用程序在表(行)上加上某种锁后,其他应用程序是否能够在表(行)上加上相应的锁,如果能够加上,说明这两种锁是兼容的,否则说明这两种锁不兼容,不能对同一数据对象并发存取。
下表为oracle数据库tm锁的兼容矩阵(y=yes,表示兼容的请求; n=no,表示不兼容的请求;-表示没有加锁请求):

表五:oracle数据库tm锁的相容矩阵

一方面,当oracle执行select…for update、insert、update、delete等dml语句时,系统自动在所要操作的表上申请表级rs锁(select…for update)或rx锁(insert、update、delete),当表级锁获得后,系统再自动申请tx锁,并将实际锁定的数据行的锁标志位置位(指向该tx锁);另一方面,程序或操作人员也可以通过lock table语句来指定获得某种类型的tm锁。下表是笔者总结了oracle中各sql语句产生tm锁的情况:

表六:oracle数据库tm锁小结

我们可以看到,通常的dml操作(select…for update、insert、update、delete),在表级获得的只是意向锁(rs或rx),其真正的*粒度还是在行级;另外,oracle数据库的一个显著特点是,在缺省情况下,单纯地读数据(select)并不加锁,oracle通过回滚段(rollback segment)来保证用户不读"脏"数据。这些都提高了系统的并发程度。
由于意向锁及数据行上锁标志位的引入,减小了oracle维护行级锁的开销,这些技术的应用使oracle能够高效地处理高度并发的事务请求。
4 db2多粒度*机制的监控
在db2中对锁进行监控主要有两种方式,第一种方式是快照监控,第二种是事件监控方式。
4.1 快照监控方式
当使用快照方式进行锁的监控前,必须把监控锁的开关打开,可以从实例级别和会话级别打开,具体命令如下:

db2 update dbm cfg using dft_mon_lock on(实例级别)
db2 update monitor switches using lock on(会话级别,推荐使用)
当开关打开后,可以执行下列命令来进行锁的监控
db2 get snapshot for locks on ebankdb(可以得到当前数据库中具体锁的详细信息)
db2 get snapshot for locks on ebankdb
fri aug 15 15:26:00 jinan 2004(红色为锁的关键信息)


             database lock snapshot
database name                              = dev
database path                              = /db2/dev/db2dev/node0000/sql00001/
input database alias                       = dev
locks held                                 = 49
applications currently connected           = 38
agents currently waiting on locks          = 6
snapshot timestamp                         = 08-15-2003 15:26:00.951134
application handle                         = 6
application id                             = *local.db2dev.030815021007
sequence number                            = 0001
application name                           = disp+work
authorization id                           = sapr3
application status                         = uow waiting
status change time                         =
application code page                      = 819
locks held                                 = 0
total wait time (ms)                       = 0
application handle                         = 97
application id                             = *local.db2dev.030815060819
sequence number                            = 0001
application name                           = tp
authorization id                           = sapr3
application status                         = lock-wait
status change time                         = 08-15-2003 15:08:20.302352
application code page                      = 819
locks held                                 = 6
total wait time (ms)                       = 1060648
  subsection waiting for lock              = 0
  id of agent holding lock                 = 100
  application id holding lock              = *local.db2dev.030815061638
  node lock wait occurred on               = 0
  lock object type                         = row
  lock mode                                = exclusive lock (x)
  lock mode requested                      = exclusive lock (x)
  name of tablespace holding lock          = psapbtabd
  schema of table holding lock             = sapr3
  name of table holding lock               = tplognames
  lock wait start timestamp                = 08-15-2003 15:08:20.302356
  lock is a result of escalation           = no
list of locks
  lock object name            = 29204
node number lock is held at = 0
object type                 = table
tablespace name             = psapbtabd
table schema                = sapr3
table name                  = tplognames
mode                        = ix
status                      = granted
lock escalation             = no 


db2 get snapshot for database on dbname |grep -i locks(unix,linux平台)


locks held currently                       = 7
lock waits                                 = 75
time database waited on locks (ms)         = 82302438
lock list memory in use (bytes)            = 20016
deadlocks detected                         = 0
lock escalations                           = 8
exclusive lock escalations                 = 8
agents currently waiting on locks          = 0
lock timeouts                              = 20

db2 get snapshot for database on dbname |find /i "locks"(nt平台)
db2 get snapshot for locks for applications agentid 45(注:45为应用程序句柄)


application handle                         = 45
application id                             = *local.db2dev.030815021827
sequence number                            = 0001
application name                           = tp
authorization id                           = sapr3
application status                         = uow waiting
status change time                         =
application code page                      = 819
locks held                                 = 7
total wait time (ms)                       = 0
list of locks
lock object name            = 1130185838
node number lock is held at = 0
object type                 = key value
tablespace name             = psapbtabd
table schema                = sapr3
table name                  = tplognames
mode                        = x
status                      = granted
lock escalation             = no
lock object name            = 14053937
node number lock is held at = 0
object type                 = row
tablespace name             = psapbtabd
table schema                = sapr3
table name                  = tplognames
mode                        = x
status                      = granted
lock escalation             = no


也可以执行下列表函数(注:在db2 v8之前只能通过命令,db2 v8后可以通过表函数,推荐使用表函数来进行锁的监控)
db2 select * from table(snapshot_lock('dbname',-1)) as locktable监控锁信息
db2 select * from table(snapshot_lockwait('dbname',-1) as lock_wait_table监控应用程序锁等待的信息
4.2 事件监控方式:
当使用事件监控器进行锁的监控时候,只能监控死锁(死锁的产生是因为由于锁请求冲突而不能结束事务,并且该请求冲突不能够在本事务内解决。通常是两个应用程序互相持有对方所需要的锁,在得不到自己所需要的锁的情况下,也不会释放现有的锁)的情况,具体步骤如下:
db2 create event monitor dlock for deadlocks with details write to file '$home/dir'
db2 set event monitor dlock state 1
db2evmon -db dbname -evm dlock看具体的死锁输出(如下图)


      deadlocked connection ...
  deadlock id:   4
  participant no.: 1
  participant no. holding the lock: 2
  appl id: g9b58b1e.d4ea.08d387230817
  appl seq number: 0336
  appl id of connection holding the lock: g9b58b1e.d573.079237231003
  seq. no. of connection holding the lock: 0126
  lock wait start time: 06/08/2005 08:10:34.219490
  lock name       : 0x000201350000030e0000000052
  lock attributes : 0x00000000
  release flags   : 0x40000000
  lock count      : 1
  hold count      : 0
  current mode    : ns  - share (and next key share)
  deadlock detection time: 06/08/2005 08:10:39.828792
  table of lock waited on      : orders
  schema of lock waited on     : db2inst1
  tablespace of lock waited on : userspace1
  type of lock: row
  mode of lock: ns  - share (and next key share)
  mode application requested on lock: x   - exclusive
  node lock occured on: 0
  lock object name: 782
  application handle: 298
  deadlocked statement:
    type     : dynamic
    operation: execute
    section  : 34
    creator  : nullid
    package  : syssn300
    cursor   : sql_cursn300c34
    cursor was blocking: false
    text     : update orders  set totaltax = ?, totalshipping = ?, 
        locked = ?, totaltaxshipping = ?, status = ?, field2 = ?, timeplaced = ?, 
        field3 = ?, currency = ?, sequence = ?, totaladjustment = ?, ormorder = ?, 
        shipascomplete = ?, providerordernum = ?, totalproduct = ?, description = ?, 
        member_id = ?, orgentity_id = ?, field1 = ?, storeent_id = ?, ordchnltyp_id = ?, 
        address_id = ?, lastupdate = ?, comments = ?, notificationid = ? where orders_id = ?
  list of locks:
      lock name                   : 0x000201350000030e0000000052
      lock attributes             : 0x00000000
      release flags               : 0x40000000
      lock count                  : 2
      hold count                  : 0
      lock object name            : 782
      object type                 : row
      tablespace name             : userspace1
      table schema                : db2inst1
      table name                  : orders
      mode                        : x   - exclusive
      lock name                   : 0x00020040000029b30000000052
      lock attributes             : 0x00000020
      release flags               : 0x40000000
      lock count                  : 1
      hold count                  : 0
      lock object name            : 10675
      object type                 : row
      tablespace name             : userspace1
      table schema                : db2inst1
      table name                  : bkorditem
      mode                        : x   - exclusive(略去后面信息)

5 oracle 多粒度*机制的监控
为了监控oracle系统中锁的状况,我们需要对几个系统视图有所了解:
5.1 v$lock视图
v$lock视图列出当前系统持有的或正在申请的所有锁的情况,其主要字段说明如下:

表七:v$lock视图主要字段说明

其中在type字段的取值中,本文只关心tm、tx两种dml锁类型;
5.2 v$locked_object视图
v$locked_object视图列出当前系统中哪些对象正被锁定,其主要字段说明如下:

表八:v$locked_object视图字段说明

5.3 oracle锁监控脚本
根据上述系统视图,可以编制脚本来监控数据库中锁的状况。
5.3.1 showlock.sql
第一个脚本showlock.sql,该脚本通过连接v$locked_object与all_objects两视图,显示哪些对象被哪些会话锁住:


/* showlock.sql */
column o_name format a10
column lock_type format a20
column object_name format a15
select rpad(oracle_username,10) o_name,session_id sid,
decode(locked_mode,0,'none',1,'null',2,'row share',
3,'row exclusive',4,'share',5,'share row exclusive',6,'exclusive') lock_type,
object_name ,xidusn,xidslot,xidsqn
from v$locked_object,all_objects
where v$locked_object.object_id=all_objects.object_id;
5.3.2        showalllock.sql

第二个脚本showalllock.sql,该脚本主要显示当前所有tm、tx锁的信息;


/* showalllock.sql */
select sid,type,id1,id2,
decode(lmode,0,'none',1,'null',2,'row share',
3,'row exclusive',4,'share',5,'share row exclusive',6,'exclusive')
lock_type,request,ctime,block
from v$lock
where type in('tx','tm');
6 db2 多粒度*机制示例
以下示例均运行在db2 udb中,适用所有数据库版本。首先打开三个命令行窗口(db2 clp),其中两个(以下用sess#1、sess#2表示)以db2admin用户连入数据库,以操作sample库中提供的示例表(employee);另一个(以下用sess#3表示)以db2admin用户连入数据库,对执行的每一种类型的sql语句监控加锁的情况;希望读者通过这种方式对每一种类型的sql语句监控加锁的情况。(因为示例篇幅很大,笔者在此就不做了,建议读者用类似方法验证加锁情况)


/home/db2inst1>db2 +c update employee set comm=9999(sess#1)
/home/db2inst1>db2 +c select * from employee(sess#2处于lock wait)
/home/db2inst1>db2 +c get snapshot for locks on sample(sess#3监控加锁情况)

注:db2 +c为不自动提交(commit)sql语句,也可以通过 db2 update command options using c off关闭自动提交(autocommit,缺省是自动提交)
7 总结
总的来说,db2的锁和oracle的锁主要有以下大的区别:
1.oracle通过具有意向锁的多粒度*机制进行并发控制,保证数据的一致性。其dml锁(数据锁)分为两个层次(粒度):即表级和行级。通常的dml操作在表级获得的只是意向锁(rs或rx),其真正的*粒度还是在行级;db2也是通过具有意向锁的多粒度*机制进行并发控制,保证数据的一致性。其dml锁(数据锁)分为两个层次(粒度):即表级和行级。通常的dml操作在表级获得的只是意向锁(is,six或ix),其真正的*粒度也是在行级;另外,在oracle数据库中,单纯地读数据(select)并不加锁,这些都提高了系统的并发程度,oracle强调的是能够"读"到数据,并且能够快速的进行数据读取。而db2的锁强调的是"读一致性",进行读数据(select)时会根据不同的隔离级别(rr,rs,cs)而分别加s,is,is锁,只有在使用ur隔离级别时才不加锁。从而保证不同应用程序和用户读取的数据是一致的。
2. 在支持高并发度的同时,db2和oracle对锁的操纵机制有所不同:oracle利用意向锁及数据行上加锁标志位等设计技巧,减小了oracle维护行级锁的开销,使其在数据库并发控制方面有着一定的优势。而db2中对每个锁会在锁的内存(locklist)中申请分配一定字节的内存空间,具体是x锁64字节内存,s锁32字节内存(注:db2 v8之前是x锁72字节内存而s锁36字节内存)。
3. oracle数据库中不存在锁升级,而db2数据库中当数据库表中行级锁的使用超过locklist*maxlocks会发生锁升级。
4. 在oracle中当一个session对表进行insert,update,delete时候,另外一个session仍然可以从orace回滚段或者还原表空间中读取该表的前映象(before image); 而在db2中当一个session对表进行insert,update,delete时候,另外一个session仍然在读取该表数据时候会处于lock wait状态,除非使用ur隔离级别可以读取第一个session的未提交的值;所以oracle同一时刻不同的session有读不一致的现象,而db2在同一时刻所有的session都是"读一致"的。
8 结束语
db2中关于并发控制(锁)的建议
1.正确调整locklist,maxlocks,dlchktime和locktimeout等和锁有关的数据库配置参数(locktimeout最好不要等于-1)。如果锁内存不足会报sql0912错误而影响并发。
2.写出高效而简洁的sql语句(非常重要)。
3.在业务逻辑处理完后尽可能快速commit释放锁。
4.对引起锁等待(sql0911返回码68)和死锁(sql0911返回码2)的sql语句创建最合理的索引(非常重要,尽量创建复合索引和包含索引)。
5.使用 alter table 语句的 locksize 参数控制如何在持久基础上对某个特定表进行锁定。检查syscat.tables中locksize字段,尽量在符合业务逻辑的情况下,每个表中该字段为"r"(行级锁)。
6.根据业务逻辑使用正确的隔离级别(rr,rs,cs和ur)。
7.当执行大量更新时,更新之前,在整个事务期间锁定整个表(使用 sql lock table 语句)。这只使用了一把锁从而防止其它事务进行这些更新,但是对于其他用户它的确减少了数据并发性。