1. 数据一致性与锁
为什么要有锁机制?其背后的核心逻辑在于“保证数据的一致性”。
当数据被应用程序修改时,我们必须要保证修改后的数据具有一致性。在SAP系统中,将一致的数据状态从一个状态变动到另一个一致状态的时间跨度被称为LUW(逻辑工作单元)。
如果在LUW中发生了错误,系统将会自动撤销所有的操作,并将数据的状态回滚到初始的状态。这也就是rollback。
在SAP系统中,有两种类型的LUW,也即DB LUW和SAP LUW。
- DB LUWs是通过database系统实现的
- SAP LUWs是通过过特殊的SAP编程技术实现的
相应的,对应两种不同的LUW, SAP 设计了两种不同类型的锁。
- DB Lock - 数据库系统设定加锁
- SAP Lock - 在ABAP程序中加锁
在本文中,我将结合实际的开发经验,回顾和总结SAP Lock这个技术点。
2. SAP Locks基础概念
2.1 锁服务器
理论上讲,SAP Locks应当与SAP LUW的生命周期保持一致。
因为SAP系统可以有若干个应用服务器,为了应对不同应用服务器发送的数据请求,必须要有一个独立于应用服务器外的一个application server来处理锁的生命周期,我们通常将这个server称之为锁服务器(lock server)。
2.2 锁对象
SAP Locks的实现是基于lock object这个物理实体。通过SE11我们可以完成对于锁对象的定义。
通过锁对象,我们可以实现对于一行或多行数据的锁定,可以实现对多个数据表中数据的同时锁定(通过在定义锁对象时的外键依赖foreign key dependencies)。
当SE11定义完锁对象时,系统对自动生成一对锁函数,用于加锁和解锁。
下图展示了一个示例的锁对象:ZEGG_DOC_KEY。
有关锁对象的几点经验:
1. 锁对象中的主表,可以是一个实际上的DB表,也可以是一个结构。也就是说,SAP Lock其实是一种逻辑锁,有可能你锁定的表条目在DATABASE上根本就不存在。
2. SAP系统在应用服务器层面有一个全局的LOCK TABLE (SM12查看),用来管理逻辑锁来锁定相关的表条目,这个LOCK TABLE是cross-client的。如果,你的锁对象中,不包含MANDT这种client字段,加锁时,会对所有client上的这条数据进行锁定。
3. 如果primary table对应的structure上含有MANDT字段,在生成锁对象时,其对应的加锁函数上,会默认使用SY-MANDT值作为初始值。
4. SAP Lock支持缺省锁定,若只提供了了部分lock key,则会对缺省的key值全部锁定。举例,若lock key为 BURKS, BELNR 和GJAHR,加锁时,仅提供了 BUKRS = '1000' 其余值缺省,此时则会锁定所有 BUKRS = '1000' 的条目。同理,若加锁时,未提供任何一个lock paramter,则会锁定整个锁对象对应表。
2.3 加锁
加锁时,加锁函数会访问锁服务器的lock table,通过向lock table中写入相应条目来完成上锁动作。如果lock table中已经存在相应的锁条目而无法完成锁定,则会报出异常FOREIGN_LOCK。
下面是一个sample的加锁函数。
在使用加锁函数时,除了key字段外,还要关注几个技术参数。
2.3.1 mode_dbtab
mode_dbtab控制加锁的模式,基础模式有三种,也即E,X,S。
- 模式E:当更改数据的时候设置为此模式。WriteLock(exclusive lock)
- 模式X:和E类似,但是不允许累加,完全独占。Enhancedwrite lock (exclusive lock without cumulating)
- 模式S:本身不需要更改数据,但是希望显示的数据不被别人更改。Read Lock(Shared Locked)
三种模式之间的依赖关系如下:
是否允许第二次加锁模式(同一进程内) | |||
第一次加锁模式 | S | E | X |
S | 允许 | 允许 | 不允许 |
E | 允许 | 允许 | 不允许 |
X | 不允许 |
在同一进程内,在加了S锁或E锁后,仍然可以继续加S锁或E锁。
是否允许第二次加锁模式(不同进程之间) | |||
第一次加锁模式 | S | E | X |
S | 允许 | 不允许 | 不允许 |
E | 不允许 | ||
X | 不允许 |
在不同进程之间,在加了S后,仅可以继续加S锁。
S锁与E锁的经验小结:在仅读取数据时,加S锁,这样保证其它session中,仍然可加S锁读取数据时;在修改数据时,加E锁,这样保证修改数据时,对于数据的唯一锁定。
2.3.2 _scope
_scope参数定义了锁持续的时间,也即加锁后,何时会自动解锁。下面,我们引用SAP官方文档中的一张图,来进一步解释下这个概念。
在开始一个SAP LUW时,系统会创建两个Lock Owner(dialog owner和update owner),_scope参数用于说明这个lock归属于那个lock owner来控制。
值 | 含义 |
_scope = 1 | 锁定仅属于对话所有者(dialog owner),因此仅存在于对话事务中。当调用DEQUEUE函数或事务结束时会解锁,COMMIT WORK或ROLLBACK WORK,并不会让锁失效。 |
_scope = 2 | 锁只属于更新所有者(update owner),当在更新任务CALL FUNCTION '...' IN UPDATE TASK和COMMIT WORK时,锁会被继承至此任务中。当更新事务完成时,锁会被释放。 可以使用ROLLBACK WORK,在锁被传递到更新之前释放锁。 注意:仅当调用了CALL FUNCTION '...' IN UPDATE TASK并结合COMMIT WORK时,锁才会被自动释放。仅使用COMMIT WORK并不会释放锁。
|
_scope = 3 | 该锁属于两位所有者( dialog owner 和 update owner)。换句话说,它结合了两者的行为。这个锁在两位所有者中的最后一个释放它后被取消。 |
2.3.3 _wait 和 _collect
_wait 和 _collect 参数一般使用的较少,其含义如下:
- _wait :表示如果对象已经被锁定,是否等待后再尝试加锁,最大的等待时间由系统参数 ENQUE/DELAY_MAX控制。
- _COLLECT:表示是否收集后进行统一提交。COLLECT 是一种缓存与批处理方法,即如果指定了Collect,加锁信息会放到Lock Container 中,Lock Container实际上是一个funciton Group控制的内存区域,如果程序中加了很多锁,锁信息会先放到内存中,这样可以减少对SAP锁管理系统访问。若使Lock Container中的锁生效,需执行FLUSH_ENQUEUE 这个Funciton,将锁信息更新到锁管理系统中,此时加锁操作生效,使用函数RESET_ENQUEUE可以清除Lock Container中的锁信息。
2.4 解锁
SAP锁可以通过删除锁表中的相关条目来手动释放(SM12)。
使用函数模块ENQUEUE设置SAP锁定时,传递给输入参数_SCOPE的值决定了锁定的持续时间。根据形式参数_SCOPE的值,SAP锁定可以如下释放:
加锁值 | 锁的释放方式 |
_scope = 1 | 通过调用DEQUEUE函数释放;或等到对话程序结束时自动释放(包含的场景例如:程序自动结束、发生TYPE为A或者X的message、使用语句LEAVE PROGRAM,LEAVE TO TRANSACTION,或者在命令行输入/n回车以后)。 |
_scope = 2 | 在COMMIT WORK前通过DEQUEUE函数释放(但这没什么业务意义,因为_scope = 2时,我们一定是希望锁持续至COMMIT WORK触发的更新任务执行完成。唯一有效的场景是,在加完_scope = 2的锁后(COMMIT WORK执行前),程序发生了异常,此时捕获异常后,在CATCH分支需要DEQUEUE手动解锁,否则这个锁会在程序结束后,继续存在)。 通过IN UPDATE TASK任务结合commit work一起使用,在COMMIT WORK执行结束后,自动解锁。 |
_scope = 3 | 更新函数和对话程序都必须释放锁:
最终锁释放由最后释放锁的动作决定。 |
注意:如果要使用函数模块 DEQUEUE 来在更新之外释放 SAP 锁定,那么形式参数 _SCOPE 必须被赋予一个大于或等于与函数模块 ENQUEUE 的同一形式参数传入的值。
下图是一个默认生成的dequeue函数,可以看到,dequeue时,其默认的scope是3。
2.5 上锁的一般步骤
先上锁,上锁成功之后,从数据库取数据,然后更改数据,接着更新到数据库,最后解锁。按照这个步骤,才能保证更改完全运行在锁的保护机制下。
3. 小结
本文总结了SAP Lock的工作原理,并分享了实际使用中的经验。希望本文对你有帮助!