光洋山
作者光洋山·2017-01-14 19:01
数据库架构师·金融科技公司

深度剖析GBase 8t 锁机制

字数 8236阅读 3024评论 0赞 1

最近遇到很多GBase 8t的初学者,提出各类关于GBase 8t锁机制相关的问题,结合大家的问题,我认真再整理GBase 8t锁机制相关的基础知识及使用中的相关问题的处理。本文将文大家讲述如下锁机制相关的内容:

  1. 锁类型
  2. 锁粒度
  3. 锁的管理和分配
  4. 锁等待设置
  5. 死锁
  6. 隔离级别
  7. 数据库日志模式
  8. 锁冲突监控及分析
  9. 锁冲突优化建议

1. 锁类型

GBase 8t锁类型可以分为4类:
1.共享锁--S锁
共享锁简称S锁,可以防止其他用户对记录进行修改。只要对象上没有排它锁-X锁时,可以在该对象上加共享锁。
2.提升锁--Update锁
Update锁,简称U锁,当使用for update 游标cursor时,自动在row记录上创建U锁。当记录上没有x锁时,才能创建U锁。当游标操作真正执行update语句时,该记录的U锁将自动提升为X锁。
3.排它锁--x锁
X锁,又称独占锁,顾名思义就是不能和其他锁类型同时存在同一个对象上。比如,当表的一行记录上有x锁时,其他任何锁都不能在该记录上存在。
4.Intent锁--I锁
Intent锁为GBase 8t内部自动创建的锁。当表的某一行记录执行udpate时,该记录上有一个X锁,此时,系统内部将自动在该表Table上增加一个X锁,此时内部使用IX标记锁类型。用来防止表在访问的过程中被修改(drop/alter/rename等)
锁兼容性矩阵图
锁矩阵图

锁矩阵图

如上锁矩阵图,当对象上有S锁时,可以加S锁,U锁,但不能再加X锁。

2. 锁粒度

锁粒度也称锁对象或者锁范围,表示封锁的作用范围,例如,一条记录上的锁,只作用于该记录。
GBase8t数据库的锁机制可以锁定不同粒度对象:
锁粒度

锁粒度

数据库级锁 Database-level Locks
当我们通过CONNECT DATABASE 或者CREATE DATABASE语句访问数据库时,系统都将自动在该数据库上加上一个共享(S)锁。这样可以防止其他的用户删除数据库或者防止在该数据库上加排它(X)锁。

表级锁
就是指锁定的对象是一个表。可以通过如下语句显式的对表加锁和释放锁:

BEGIN WORK;
LOCK TABLE tab1 IN EXCLUSIVE MODE;
LOCK TABLE tab2 IN SHARE MODE;
UNLOCK TABLE tab1;

当执行如下一些DDL语句时,GBase8t会自动对表进行加锁。如ALTER FRAGMENT、ALTER INDEX、ALTER TABLE、CREATE INDEX (如果没有使用ONLINE模式)、DROP INDEX (如果没有使用ONLINE模式)、RENAME COLUMN、RENAME TABLE。
当整个表或者表中大部分数据需要更新时,使用表级锁效率高。
页级锁 Page Locks
GBase8t物理上把多行记录存放在数据页(Page)上,页级锁就是指锁的对象是一个数据页,当采用页级锁访问记录时, GBase8t会自动对访问的数据页进行加锁。当按物理顺序访问和更新多条记录时,使用页级锁效率较高。
可以通过如下方式指定页级锁模式:
ALTER TABLE tab1 LOCK MODE (PAGE);
CREATE TABLE tab1(...) LOCK MODE PAGE ;
CREATE TABLE时若不指定锁模式,将采用ONCONFIG参数DEF_TABLE_LOCKMODE来指定表的锁模式。
行级锁 Row Locks
关系型数据库的数据是按行来管理的,所以行级锁很容易理解。在OLTP系统中,行级锁使用广泛,只需要锁定所访问的少数记录情况,使用行级锁效率高。
可以通过如下方式指定行级锁模式:

ALTER TABLE tab1 LOCK MODE  (ROW);
CREATE TABLE tab1(...) LOCK MODE ROW ;

CREATE TABLE时若不指定锁模式,将采用ONCONFIG参数DEF_TABLE_LOCKMODE来指定表的锁模式。
Index key 锁
索引采用B+树的存储结构,所以对索引的锁管理就是管理索引的Key值。数据库采用开、闭区域的方式进行索引的锁管理,示例如下:

Create table tab1 (c1 int,c2 int) lock mode row;
Create unique index idx_tab1 on tab1(c1);
Insert into tab1 values(1,2);
Insert into tab1 values(2,2);
Insert into tab1 values(3,2);

假设表中只有如上3行记录,当执行update tab1 set c1=0 where c1>=3;语句时,GBase8t将对索引key值为3的记录进行闭区间加锁,意味着此时不能新增key值>3的记录。若此时执行insert into tab1 values(4,2);则会提示Index锁冲突错误。

3. 锁的管理和分配

锁内存管理
GBase8t采用全局管理的封锁机制,在共享内存中分配一块内存集中标记锁的使用情况,每个锁结构中保存锁的拥有者、锁定的对象(是表、记录、还是行)、锁的类型等。GBase 8t中每个锁占用128 byte大小。
锁内存结构图

锁内存结构图

锁的参数设置
ONCONFIG LOCKS
定义数据库启动时,初始化锁的个数。LOCKS最大初始化大小如下:
32-Bit 系统
LOCKS 8000000
64-Bit 系统
LOCKS 500000000
最小配置值为2000。

锁的动态分配
GBase8t对锁的管理采用动态分配机制,初始化时在常驻内存段申请固定大小的内存存储锁。如果锁的分配数超过了初始化参数LOCKS设置的值,数据库服务器将自动增加锁的个数,此时GBase8t将使用虚拟段来存储新分配的锁。每次以已分配锁的数量的倍数分配新的锁。在online.log日志里有如下信息,

04:17:54  dynamically allocated 256000 locks

当锁的数量增长到最大支持的数后,GBase8t还可以分配额外的锁,一共可以再增长15次,每次可以申请100000个锁。故GBase8t支持的最大锁数量如下:
32-Bit: 8.000.000 + (15x100.000) = 9,500,000
64-Bit: 500.000.0000 + (15x100.000) = 501,500,000

表默认锁模式
ONCONFIG DEF_TABLE_LOCKMODE
DEF_TABLE_LOCKMODE参数用来指定创建表时默认的锁模式,ONCONFIG默认值为page,对于OLTP系统,当安装、初始化数据库后,尽量将其修改为row。

查看表锁模式
可以通过如下3种方式查看指定表的锁模式--page or row级锁

  1. oncheck -pt db:tabname
  2. dbschema -d db -t tabname -ss
  3. select locklevel from systables where tabname="tabanme";

修改表锁模式
alter table tabname lock mode(row);

可以通过如下脚本批量生成所有表的修改语句:

unload to alter_lock.sql
select "ALTER TABLE "||tabname||" lock mode(row);" 
from systables
where tabid>99
and tabtype='T'
and locklevel="P";

锁的生命周期
事务结束时释放锁资源。
程序控制数据库锁的有效期。当数据库关闭后,数据库锁将被释放。根据数据库是否使用了事务的情况,表锁的有效期不同
如果数据没有使用事务(没有事务日志,也不使用commit work语句),显示对一个表lock,当执行unlock table时,锁将被释放;当数据库使用了事务,事务结束时,将释放事务所有的锁 table, row, page, and index locks。

数据库工具和锁情况
1.dbexport/dbimport
将独占数据库,即在数据库上自动加x锁。
2.onunload
共享锁
3.load/unload
无需加锁
4.dbload
当使用-k选项时,将在表上增加x锁,默认无需加锁。
5.oncheck -cd/-cD
共享锁
6.oncheck -ci/-cI
当表采用行级锁时,无需加锁。当表采用Page锁时,将加S锁。
7.oncheck -pt
无需加锁
8.oncheck -pT
共享锁

4. 锁等待设置

当遇到锁冲突时,我们可以修改锁的等待模式。假如Session1正在修改表的第1条记录,此时Session2也尝试修改相同的记录,那么默认情况下,Session2会遇到锁冲突,返回错误。我们可以通过设置Session2的锁等待时间模式来确定是否立即返回锁冲突错误,还是等待n秒后再次尝试。GBase8t提供如下几种模式,我们可以在Session中加入如下代码:

SET LOCK MODE TO NOT WAIT;
SET LOCK MODE TO WAIT 5;
SET LOCK MODE TO WAIT;

GBase8t默认采用NOT WAIT模式。如果是等待模式,那么GBase8t将在等待时间内不断尝试,直到成功为止。
我们可以通过onstat -g sql来监控Session采用的锁等待模式。如下所示的输出中,有三种不同的模式,Wait表示永远等,Wait 5表示最多等待5秒,Not Wait不等立即返回。
锁等待

锁等待

如果通过onstat -g sql命令发现有部分应用没有设置合理的锁等待模式,而是采用默认的NOT WAIT模式,将有大量的锁错误,导致很多事务回滚。onstat -p输出的lokwaits表示锁等待次数,rollbk为事务回滚次数,如果rollbk、lokwaits值均较大,那么需要监控事务使用的锁等待模式是否合理,确定是否由于锁冲突导致事务回滚。如果是锁导致,我们通过设置锁等待模式来减少事务由于锁冲突回滚的情况,通过在程序中加入:set lock mode to wait 5;来设置等待5秒。
如果不能直接修改应用程序代码,可以通过如下方式修改数据库默认的锁等待模式。

CREATE PROCEDURE public.sysdbopen()
 SET LOCK MODE TO WAIT 5;
END PROCEDUR

通过以上代码,连接到该数据库的Session如果没有在Session代码里设置锁等待模式,将自动使用SET LOCK MODE TO WAIT 5;来设置锁等待模式。

提示:public对所有用户有效,可以设置指定用户:username.sysdbopen,只对该用户有效。

5. 死锁

死锁由于多个用户之间互相等待资源,而等待的资源都被对方占用。对于死锁,GBase 8t将自动监测及自动解除死锁。
如下示例,模拟演示死锁的发生。
日志数据库 demodb with log

Create Table
create table test_lock(c1 integer,c2 char(12)) lock mode row;
Create unique index idx_test_lock on test_lock(c1);

测试数据

insert into test_lock values(1,'abc');
insert into test_lock values(2, 'abc');

模拟如下2个session,可以使用SQL开发工具,逐条并行执行。
死锁

死锁

session1 先执行第一条update语句,然后session 2执行第一条update语句
然后sesion1执行select语句;session再执行select 语句后将提示243,143死锁问题

6. 隔离级别

隔离级别是用来控制用户读取记录时,锁的使用控制机制。通过不同的隔离级别控制,可以读取到正在修改的脏数据,也可以读取修改前的值,也可以约束读取必须提交的值。
GBase 8t支持如下5种不同的隔离级别关于隔离级别的详细描述,请参考作者的blog内容。
1.dirty read
set isolation to dirty read;
脏读隔离级别下,读取记录无论是否有x锁,都可以读取内存值,无需加锁。

2.committed read
set isolation to committed read;
提交读,读取已提交的数据,若遇到对象正在修改x锁,将等待或者报锁冲突,但该隔离级别下,不会对读取的记录加任何锁。

3.last committed read
set isolation to committed read last committed;
乐观读机制,在committed read基础上,保障读取的记录的一定是commit的,当读取记录正在修改x锁,在该隔离级别上,将读取最后一次提交commit的记录。内部实现需要读取逻辑日志信息来获得last commit值。

4.cursor 保持
set isolation to cursor stability;
增加U锁在读取的记录上,游标处理update时,将提升为X锁。

5.repeatable read
可重复读,对读取的记录加s锁,相当于ANSI标准的 serializable隔离级别。

7. 数据库日志模式

数据库日志模式和隔离级别对照表

日志模式对照表

日志模式对照表

不同的日志模式,采用不同默认的隔离级别。

8. 锁冲突监控及分析

监控锁
在实际的生产系统中,GBase8t常见的锁问题包括锁等待、死锁等。我们通过onstat -p的输出可以查看到系统总体上锁的使用情况,包括锁申请数,锁等待数以及死锁情况。可以进一步通过onstat -g ppf查看到每一个表使用锁的情况。另外,可以通过SQL语句监控锁的使用情况。

锁监控锁演示
演示行级锁,
创建表test_lock,设置为行级锁,并创建一个唯一索引,插入3条测试数据:

Create table test_lock (c1 int,c2 int) lock mode row;
Create unique index idx_test_lock on test_lock(c1);
Insert into test_lock values(1,2);
Insert into test_lock values(2,2);
Insert into test_lock values(5,2);

通过dbaccess进入数据,update第二条记录

Begin work;
Update test_lock set c2=10 where c1=2;

监控锁的情况如下,Session 192使用了3个锁。onstat -k的输出中,
tblsnum=100002的是系统表保持数据库信息表,rowid=206是指testdb数据库,由于连接到该数据库,故会在testdb上加上一个数据库级锁。
Tblsnum=14000a5为表test_lock,在该表上有2个锁,rowid=0标识是一个表级锁,再看该锁的类型为IX,表示为意向锁(Intent)――由于update语句修改了表的记录,故Informix内部会自动对表加X锁。Rowid=102是指表test_lock的第二行记录,就是update 语句修改的记录,在记录上加上X锁。

查询是“谁”加了锁
如下介绍一个常用的通过onstat命令找到锁的拥有者,解决对象被锁的示例。
按如下步骤找到对表test_lock加锁的进程,然后杀掉进程,释放锁。
Step 1. 查询表test_lock在数据库中16进制的Partnum。
select hex(partnum) from systables where tabname="test_lock";
返回值为:0x00800100
Step 2. 通过表的partnum找到目前正在以锁定方式访问该表的用户线索

Step 3. 通过锁的所有者b3a56d80找到锁的Session 信息,session id为37的使用者对表test_lock加上了5个锁。通过wait项可以看到由于b3a56d80对数据加锁导致了用户b9885e38等待。

Step 4. 通过Session ID 37,可以得到具体的对表上锁的SQL及相关信息.
onstat -g ses 37 和onstat -g sql 37
我们可以查到该Session的具体情况,由那个应用程序发起的SQL,使用了哪些锁。如果需要终止占用锁的session,可以使用onmode -z命令杀掉session:onmode -z 37

当前系统锁等待情况
onstat -u onstat -k
可以通过如下方式来监控锁情况。

9. 锁冲突优化建议

1.锁粒度的选择
不同场景下选择不同的锁粒度,DBA管理任务,操作大批量数据时,可以采用表级锁或者page锁。
当OLTP并发较大的业务系统,建议采用row锁。减少锁冲突的可能性。

2.锁等待设置
为不同的任务设置不同的锁等待超时,而不是直接遇到锁冲突导致事务大量回滚。
set lock mode to wait 5;

3.隔离级别的选择
根据隔离级别与锁的关系,灵活为不同的session设置不同的隔离级别。
建议业务系统中采用committed read即可,尽量不要使用repeatable read。
last committed read可以避免committed read不足,但是效率没有committed read高。
特别是大数据表查询的报表业务,不要采用last committed read,建议采用dirty read,如果可能。

4.顺序扫描带来恶果

许多应用程序的SQL语句由于没有正确的使用好索引INDEX,导致对整个表进行了锁定,导致并发遇到锁的问题。需要通过SQL优化和创建合理的索引,避免顺序扫描带来的锁冲突问题。
由于顺序扫描导致的锁等待问题:
创建表test_lock,采用行级锁,并在c1字段上创建索引,插入3行测试数据。

create table test_lock (c1 int,c2 int,c3 char(10)) lock mode row;
create index idx_test_lock on test_lock(c1);

insert into test_lock values(1,1,'abc');
insert into test_lock values(2,2,'abc');
insert into test_lock values(3,3,'abc'); 

Session 1通过索引扫描来修改第2行记录。

Begin work;
update test_lock set c3='new' where c1=2;

此时,Session 1将在第二行记录上加X锁。
Session 2通过c2字段以顺序扫描的方式来修改第3行记录。

Begin work;
update test_lock set c3='new' where c2=3;

由于Session 2采用顺序扫描方式,扫描过程中发现第2行记录加上了X锁,故提示锁冲突错误:

244: Could not do a physical-order read to fetch next row.  
   107: ISAM error:  record is locked.

如果在c2字段上加上索引,或者通过c1=3来修改第三行记录,那么就不会出现锁的问题。

5.尽快释放锁
应用程序优化避免过长占用锁,锁是一个公共资源,需要尽快释放锁资源,减少等待,如果每个事务都可以在微秒级别完成,那么锁就不会是我们关注的问题。所以减少锁占用的时间也是解决锁问题的关键。我们可以通过避免大事务,把大事务拆分多次提交,这样锁就能尽快释放。另外,优化应用的处理性能,如合理使用索引等,都可以加快事务的处理速度,无形中就解决了锁冲突的可能性。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

1

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广