1. 并发控制及 Lock 接口说明
在了解并发控制之前,请先了解以下以下代名词的解析:
名词 | 定义 |
---|---|
Base | 基础、基线。在时间线上如果变更 B 在变更 A 后,那么变更 B 是基于变更 A 的 |
Change-merging | 变更合并。与合并的概念相同 |
Concurrency Control | 并发控制。如何在保持数据完整性的同时协调同步事务 |
Concurrency Control Policy | 并发控制策略。应用程序在更改模型和元素时要遵循的规则,包含乐观规则和悲观规则 |
Conflict | 冲突。当两个变更集以不同的方式更改同一对象时发生,其中两个变更集都不互相基于另一个 |
Conflict-resolution | 冲突解决。选择如何解决冲突 |
Lock | 锁。访问元素及其子元素的权限 |
Merge | 合并。将变更集应用于公文包 |
Optimistic Concurrency Control | 乐观并发控制。一个可允许应用程序在没有获得锁的前提下更改元素的策略 |
Pessimistic Concurrency Control | 悲观并发控制。一个需要应用程序在获得锁的前提下更改元素的策略 |
Push | 推送变更。上传一个变更到 iModelHub 中 |
Pull | 拉取。从 iModelHub 中下载一个变更,详情请查阅 IModelDb synchronization |
Tip | 一个 iModel 的最近的版本。同时,也是时间线上的最近的一个变更 |
Transaction | 原子化地提交或放弃的一组更改,构成一个工作单元。通过调用 BriefcaseDb.saveChanges 方法提交事务,briefcase 中的多个事务合并到一个变更集中 |
Version | iModel 在其时间线中某个特定点的状态 |
并发控制是一种在多个 briefcase 之间协同更改、同时保持数据完整性的方法。并发控制在应用程序的代码中实现,并且是基于 briefcase 的身份去实现的。并发控制不应与用户访问控制(基于角色进行更改的“权利”)混淆。要进行协同更改,应用程序必须遵循两条基本规则:
- 根据 iModel 的并发控制策略,可以选择在修改模型和元素之前锁定它们
- 推送变更之前先拉取最新的变更并且对本地的更改进行合并(类似 Git 提交代码一样)
iModel 有一个并发控制策略,指定多个 briefcase 如何修改元素。策略可以规定必须使用锁,强制事务是顺序执行的(悲观的),或者可以指定带有冲突解决的变更合并策略来合并同时发生的事务的结果(乐观的)。
iModel 的并发控制策略是在 iModelHub 中通过BackendHubAccess.createNewIModel 方法的 noLocks 参数创建的。如果未指定 noLocks(默认值),则 iTwin.js 将强制要求在修改元素之前必须获得适当的锁。
一个应用程序使用 BriefcaseDb.locks 来遵循并发控制规则。
锁在 briefcase 进行更改时与 briefcase 关联,并在 briefcase 推送变更时释放(可选)。
1.1. 并发控制策略
1.1.1. 悲观并发控制
悲观并发控制策略要求必须先锁定元素,然后才能对其进行更改。锁定可以防止并发更改,并让影响相同模型和元素的 briefcase 事务在合并时而不发生冲突。
锁类型
锁(仅)应用于元素,并代表 briefcase 通过指定锁类型和元素 ID 获得锁。
有两种类型的锁:
- 独占锁:为独占访问保留元素。只有独占锁的持有者才能修改或删除元素
- 共享锁:在某个元素上持有共享锁会阻止其他 briefcase 获取该元素的独占锁
在元素上获取锁
通过提供一个或者多个元素 ID,调用锁控制接口的 BriefcaseDb.locks.acquireExclusiveLock 和 BriefcaseDb.locks.acquireSharedLock 方法获取锁。
获取锁的规则:
- 一次只有一个 briefcase 可以在一个元素上持有独占锁
- 只有当 BriefcaseDb.changeset.index 等于或者大于上次释放锁时指定的 ChangesetIndex 你才能获取独占锁。也就是说,只有在 briefcase 处于最新状态时,才能获得元素的独占锁
- 当独占锁由另一个 briefcase 持有时,无法获取元素的共享锁
- 试图获取元素上的锁(独占或共享)还需要获取其模型及其父级(如果有)上的共享锁。这是自动且递归的行为。也就是说,一个获取单个锁的请求实际上可能需要很多锁,直到层次结构的顶部,如果它们还没有被持有的话。如果任何所需的锁不可用,则不会获得任何锁。
根元素 ID 是 IModel.repositoryModelId。为方便起见,根元素上的独占锁称为模式锁。从上面的规则可以看出,要获得 iModel 的模式锁,其他 briefcase 不能持有任何锁。此外,在持有模式锁的同时,其他 briefcase 可能无法获得任何锁。
要使用带有悲观并发控制策略的 iModel,应用程序必须遵循 pull->lock->change->push 模式。
作为参考,悲观锁定规则如下:
操作 | 需要的锁类型 |
---|---|
插入元素 | 模型和父元素上的共享锁(如果存在) |
修改元素 | 独占锁 |
删除元素 | 独占锁 |
说明:
- 这些规则仅适用于对元素的直接更改。在 ElementDrivesElement 关系传播更改期间进行的间接更改不需要锁。
- 模型上的锁实际上是其建模元素上的锁,因为它们具有相同的 Id
释放锁
当 briefcase 通过 BriefcaseDb.pushChanges 方法推送更改时,锁通常会被释放,但也可以通过 retainLocks 选项保留锁。如果获得了锁,但未进行任何更改,或者放弃了所有更改,则可以通过 BriefcaseDb.locks.releaseAllLocks 方法手动释放锁。
1.1.2. 乐观并发控制
乐观并发策略允许应用程序在不获取锁的情况下修改 iModel 中的元素和模型。在这种情况下,应用程序在推送变更之前使用更改合并来协调本地更改。
在没有锁的情况下工作,可能会导致在本地编辑会话进行期间,其他应用程序向时间线添加更改集,此时 briefcase 必须合并后才能推送变更。
假设两个 briefcase 同时编辑同一元素的不同属性。假设先第一个 briefcase 先推送变更,创建变更集#1。现在,第二个 briefcase 必须拉取最新变更并执行合并后才能推送它的变更集。
冲突
在没有锁的情况下工作也会增加本地更改与即将到来的更改集重叠的可能性。当变更集合并到 briefcase 中时,变更合并算法会检查冲突。该算法在单个元素属性级别合并更改并检查冲突。在上面的示例中,两个 briefcase 更改了同一元素的不同属性,这不是冲突。同样,两个 briefcase 都将属性设置为相同的值,或者两个 briefcase 都删除一个元素,这并不是冲突。如果两个 briefcase 将相同的属性设置为不同的值,或者一个 briefcase 修改元素的属性而另一个 briefcase 删除元素,则会发生冲突。
如果发现冲突,更改合并算法将应用 iModel 的冲突解决策略(conflict-resolution policy)。
本地更改 | 远程更改 | 解决方法 |
---|---|---|
update | update | RejectIncomingChange(拒绝传入的更改) |
update | delete | AcceptIncomingChange (reject not support)(接收传入的更改) |
delete | update | RejectIncomingChange (accept not supported)(拒绝传入的更改) |
属性级别的更改合并是非常细粒度的,因此它允许同时进行多种更改,而不会产生冲突。还可以使用模式指定在更高级别上检查冲突的规则。
1.2. 变更集和模式变更
在将一个模式导入 briefcase 之前,必须获取模式锁。此外,模式更改必须隔离在专用更改集中,与其他类型的更改分开。这两种并发控制策略都是如此。要导入一个模式,应用程序必须:
- 拉取并合并变更以与 iModel 的最近版本同步
- 推送任何的本地变更到 iModelHub 中
- 获取模式锁
- 在本地事务中执行模式的导入
- 将模式导入的结果作为变更集推送到 iModelHub
- 释放模式锁
1.3. iModel 锁接口
接口一、获取 iModel 锁
GET https://api.bentley.com/imodels/{id}/locks[?$skip][&$top][&briefcaseId]
获取 iModel 锁。锁代表可以修改 iModel 特定格式数据的权力。了解更多关于锁的信息请查阅 https://www.itwinjs.org/learning/backend/concurrencycontrol/#pessimistic-concurrency-control。
说明:锁类型已从此 API 中移除,可忽略。
分页
页面大小是指所有返回实例中返回的 objectId 的总大小。如果返回多个锁实例,这并不一定意味着该实例已完成,并且可能会在不同的页面中为该特定实例返回更多 objectId。
认证
需要在请求 Header 里包含 Authorization
字段,Authorization
字段要求是有效的且带有 imodels:read
作用域的 Bearer 令牌。
想要了解更多关于授权以及如何获取 access token的细节,请参考这个https://developer.bentley.com/apis/overview/authorization/文档 。
授权
用户必须在 iModel 级别上分配 imodels_webview
的权限,以及至少在项目级别上分配 imodels_webview
权限。如果未配置 iModel 级别的权限,则用户必须在项目级别分配 imodels_write
权限。
或者,用户应该是此 iModel 所属的项目所在的组织上的组织管理员。
组织管理员必须在用户管理中至少分配以下角色之一:Account Administrator(账户管理员), Co-Administrator(联合管理员), or CONNECT Services Administrator(CONNECT 服务管理员)。想要了解更多有关于用户管理的信息,请查看 Bentley 社区 的 wiki 页面 https://communities.bentley.com/communities/other_communities/licensing_cloud_and_web_services/w/wiki/50711/user-management-2-0
注意:download
属性需要用户至少拥有 imodels_read
权限。如果用户只有 imodels_webview
权限那么 download
属性常空。
接口使用率限制
所有在 iTwin Platform 上的接口都有接口使用率限制。想了解更多相关信息请查看 https://developer.bentley.com/apis/overview/rate-limits/
请求
请求参数
参数名称 | 所在位置 | 是否必须 | 描述 |
---|---|---|---|
id | template | 是 | iModel 的 ID |
$top | query | 否 | $top 请求查询的集合结果中中要包含结果的数目。例如,仅返回第一个集合项,请提供以下查询:$top=1。如果此参数未提供则默认为100。此参数的值不能超过1000 |
$skip | query | 否 | $skip 选项请求查询集合中要跳过且不包含在结果中的项数。例如,要从第三位开始返回集合中的项目,请提供以下查询:$skip=2。 |
briefcaseId | query | 否 | 允许使用 briefcase ID 过滤锁,以及只查询某一个 briefcase 的锁 |
请求头
名称 | 是否必须 | 描述 |
---|---|---|
Authorization | 是 | 带有 imodels:read 作用域的 OAuth 访问令牌 |
Accept | 否 | 推荐设置为 application/vnd.bentley.itwin-platform.v1+json |
响应
返回 200 OK:
OK
{ "locks": [{ "briefcaseId": 2, "lockedObjects": [{ "lockLevel": "shared", "objectIds": ["0x1", "0x2", "0xab"] }, { "lockLevel": "exclusive", "objectIds": ["0x3", "0x4", "0xac"] } ] }, { "briefcaseId": 3, "lockedObjects": [{ "lockLevel": "shared", "objectIds": ["0x12", "0x21", "0xff"] }] } ], "_links": { "self": { "href": "https://api.bentley.com/imodels/5e19bee0-3aea-4355-a9f0-c6df9989ee7d/locks?$skip=0&$top=100" }, "prev": { "href": "https://api.bentley.com/imodels/5e19bee0-3aea-4355-a9f0-c6df9989ee7d/locks?$skip=0&$top=100" }, "next": { "href": "https://api.bentley.com/imodels/5e19bee0-3aea-4355-a9f0-c6df9989ee7d/locks?$skip=100&$top=100" } } }
返回 401 Unauthorized:
此响应表示请求缺少有效的身份验证凭据。访问令牌可能未提供、或是由错误的颁发者颁发、没有所需的作用域或请求头的格式不正确。
{
"error": {
"code": "Unauthorized",
"message": "Access denied due to invalid access_token. Make sure to provide a valid token for this API endpoint."
}
}
- Response 404 Not Found:
无法找到指定的 iModel
{
"error": {
"code": "iModelNotFound",
"message": "Requested iModel is not available."
}
}
- Response 422 Unprocessable Entity:
状态代码表示由于客户端错误(例如,格式错误的请求语法),服务器无法处理请求。
{
"error": {
"code": "InvalidiModelsRequest",
"message": "Cannot get Locks.",
"details": [{
"code": "InvalidValue",
"message": "'-1' is not a valid '$skip' value. '$skip' must be a non-negative integer.",
"target": "$skip"
}]
}
}
- Response 429 Too many requests:
此响应表示用户在给定的时间内发送了太多请求
{
"error": {
"code": "TooManyRequests",
"message": "More requests were received than the subscription rate-limit allows."
}
}
响应头
名称 | 描述 |
---|---|
retry-after | 超过客户端订阅的速率限制的请求数 |
接口二、更新 iModel 锁
PATCH https://api.bentley.com/imodels/{id}/locks
获取 iModel 锁。锁代表可以修改 iModel 特定格式数据的权力。了解更多关于锁的信息请查阅 https://www.itwinjs.org/learning/backend/concurrencycontrol/#pessimistic-concurrency-control。
说明:锁类型已从此 API 中移除,可忽略。
对象 ID 限制:目前一个请求最多可以有1000个对象 ID。
认证
需要在请求 Header 里包含 Authorization
字段,Authorization
字段要求是有效的且带有 imodels:modify
作用域的 Bearer 令牌。
想要了解更多关于授权以及如何获取 access token的细节,请参考这个https://developer.bentley.com/apis/overview/authorization/文档 。
授权
要释放任何锁(设置 LockLevel 值为 none)用户必须在 iModel 级别上分配 imodels_manage
的权限。如果未配置 iModel 级别的权限,则用户必须在项目级别分配 imodels_manage
权限。想要获取或释放用户拥有的锁,imodels_write
权限就足够了。
或者,用户应该是此 iModel 所属的项目所在的组织上的组织管理员。
组织管理员必须在用户管理中至少分配以下角色之一:Account Administrator(账户管理员), Co-Administrator(联合管理员), or CONNECT Services Administrator(CONNECT 服务管理员)。想要了解更多有关于用户管理的信息,请查看 Bentley 社区 的 wiki 页面 https://communities.bentley.com/communities/other_communities/licensing_cloud_and_web_services/w/wiki/50711/user-management-2-0
注意:download
属性需要用户至少拥有 imodels_read
权限。如果用户只有 imodels_webview
权限那么 download
属性常空。
接口使用率限制
所有在 iTwin Platform 上的接口都有接口使用率限制。想了解更多相关信息请查看 https://developer.bentley.com/apis/overview/rate-limits/
请求
请求参数
参数名称 | 所在位置 | 是否必须 | 描述 |
---|---|---|---|
id | template | 是 | iModel 的 ID |
请求头
名称 | 是否必须 | 描述 |
---|---|---|
Authorization | 是 | 带有 imodels:read 作用域的 OAuth 访问令牌 |
Accept | 否 | 推荐设置为 application/vnd.bentley.itwin-platform.v1+json |
Content-Type | 否 | 标识请求主体的内容类型。支持的类型为 application/json |
请求主体
参数名称 | 所在位置 | 是否必须 | 描述 |
---|---|---|---|
briefcaseId | Int32 | 否 | 要更新锁的 briefcase ID |
changesetId | String 或 null | 否 | 变更集 ID。用于标识更新锁对象的最新变更集。如果该值指向比服务器中保存的值更旧的变更集,则获取锁将失败。 |
lockedObjects | LockedObjects[] | 否 | 锁对象组成的数组 |
例子
{
"briefcaseId": 2,
"changesetId": "1f2e04b666edce395e37a795e2231e995cbf8349",
"lockedObjects": [{
"lockLevel": "shared",
"objectIds": ["0x1", "0x2", "0xab"]
},
{
"lockLevel": "exclusive",
"objectIds": ["0x3", "0x4", "0xac"]
},
{
"lockLevel": "none",
"objectIds": ["0x5", "0x6", "0xad"]
}
]
}
响应
返回 200 OK:
OK
{ "lock": { "briefcaseId": 2, "lockedObjects": [{ "lockLevel": "shared", "objectIds": ["0x1", "0x2", "0xab"] }, { "lockLevel": "exclusive", "objectIds": ["0x3", "0x4", "0xac"] } ] } }
返回 401 Unauthorized:
此响应表示请求缺少有效的身份验证凭据。访问令牌可能未提供、或是由错误的颁发者颁发、没有所需的作用域或请求头的格式不正确。
{
"error": {
"code": "Unauthorized",
"message": "Access denied due to invalid access_token. Make sure to provide a valid token for this API endpoint."
}
}
Response 403 Forbidden:
用户无权更新锁
{ "error": { "code": "InsufficientPermissions", "message": "The user has insufficient permissions for the requested operation." } }
Response 404 Not Found:
(1)无法找到指定的 iModel
{
"error": {
"code": "iModelNotFound",
"message": "Requested iModel is not available."
}
}
(2)无法找到指定的 Briefcase
{
"error": {
"code": "BriefcaseNotFound",
"message": "Requested Briefcase is not available."
}
}
(3)无法找到指定的文件
{
"error": {
"code": "FileNotFound",
"message": "Requested file is not available. File was not uploaded to file storage."
}
}
(4)无法找到指定的变更集
{
"error": {
"code": "ChangesetNotFound",
"message": "Requested Changeset is not available."
}
}
Response 409 Conflict:
(1)锁已经被其他 briefcase 占用,不能获取
{ "error": { "code": "ConflictWithAnotherUser", "message": "Lock(s) is owned by another briefcase.", "conflictingLocks": [{ "lockLevel": "shared", "objectId": "0x1", "briefcaseIds": [2, 5, 6] }, { "lockLevel": "shared", "objectId": "0x2", "briefcaseIds": [5, 6] }, { "lockLevel": "exclusive", "objectId": "0x3", "briefcaseIds": [6] } ] } }
(2)锁在更新的变更集中被更新
{ "error": { "code": "NewerChangesExist", "message": "One or more objects have been locked in a newer Changeset.", "objectIds": ["0x1", "0x2", "0x3"] } }
Response 413 Request Entity Too Large:
标识请求负载包含太多对象ID
{ "error": { "code": "RequestTooLarge", "message": "Provided 'objectIds' count exceeds the limit of 1000." } }
Response 422 Unprocessable Entity:
状态代码表示由于客户端错误(例如,格式错误的请求语法),服务器无法处理请求。
(1)非法的 iModel 请求
{
"error": {
"code": "InvalidiModelsRequest",
"message": "Cannot update Changeset.",
"details": [{
"code": "InvalidValue",
"message": "Provided 'state' value is not valid. Should be set to 'fileUploaded'.",
"target": "state"
},
{
"code": "MissingRequiredProperty",
"message": "Required property is missing.",
"target": "state"
},
{
"code": "InvalidRequestBody",
"message": "Failed to parse request body. Make sure it is a valid JSON."
}
]
}
}
(2)请求主体缺失
{
"error": {
"code": "MissingRequestBody",
"message": "Request body was not provided."
}
}
- Response 429 Too many requests:
此响应表示用户在给定的时间内发送了太多请求
{
"error": {
"code": "TooManyRequests",
"message": "More requests were received than the subscription rate-limit allows."
}
}
响应头
名称 | 描述 |
---|---|
retry-after | 超过客户端订阅的速率限制的请求数 |