# 应用锁

应用锁组件用于控制并发修改,比如协同流程并发修改和公文正文并发编辑控制。

为应用提供统一的独占锁,管理用户、资源、操作三者之间的关系。 只控制锁的状态(加锁、解锁、锁失效、锁判断),不实际对资源进行锁定,资源和操作的锁定由应用自己管理。

为同时适应单机和集群环境,提升性能,提供内存锁和数据库锁两种模式。 单机环境使用内存保存锁;集群环境使用数据库保存锁。

# 功能特性

功能一: 锁定资源 特定用户锁定资源,解锁或锁失效之前其他用户不允许访问资源。 锁定资源后指定资源只允许加锁者独占使用。

功能二: 锁定资源的操作 特定用户锁定资源的操作,解锁或锁失效之前其他用户不允许进行资源的操作。

功能三: 解锁 解除资源的所有锁或解除资源特定操作的锁。

功能四: 锁判断 判断指定用户是否可以访问资源或资源的操作。 这里会有一定歧义:对资源加锁是锁定了资源的所有操作,还是操作为空本身就是一种操作。 在这里不做处理,由应用自己解释。如果有特定的解释,请不要使用组件提供的锁判断方法,取得资源的所有锁后自己判断。

功能五: 锁过期 提供了锁过期的策略,缺省过期时间设置为8小时,以后可以进行扩展。

功能六: 锁失效 以下三种情况会导致锁失效,失效的锁将被系统自动清理。

  • 锁过期;
  • 加锁人不在线;
  • 加锁人虽然在线,但加锁以后登出过(加锁时间<登录时间)。

# 使用示例

1)应用如果要使用锁组件,确定模块编号以后,在Spring配置文件中定义一个新的bean,如下所示

<bean id="formLock" parent="lockManager">
	<property name="module" value="formLock"/>
</bean>

2)然后在自己的class中引用定义的bean即可。

<bean id="formManager" class="com.seeyon.v3x.form.FormManagerImpl">
	<property name="formLock" ref="formLock"/>
</bean>

3)业务代码层面是:“判断是否锁占用”、“加锁”、“解锁”结合使用来保证锁的有效性。

   public class FormManagerImpl {
       private long owner;
       private LockManager formLock;
       private long resourceId;
       private int action = -1;

       public long getFormLock() {
           return formLock;
       }

       public void setFormLock(LockManager formLock) {
           this.formLock = formLock;
       }

       /**占用锁*/
       public long lock(long memberId,long formId) {
           formLock.lock(memberId,formId);
       }
       
       /**判断是否正在占用锁*/
       public boolean isLock(long memberId,long formId){
           return formLock.check(memberId,formId);
       }
       
       /**解除锁占用*/
       public void unlock(long formId){
           formLock.unlock(formId);
       }
       
   }

# 最佳实践

场景1:控制编辑工作流流程业务并发操作

// 请求1:打开编辑流程页面时
long resourceId;
long userId = AppContext.currentUserId();
boolean result = lockManager.check(userId,resourceId);

if(result){
    // 可操作
    result = lockManager.lock(userId, resourceId);
}

if(!result){
    // 不可操作或加锁失败
    // 提示前端用户
}

//-------------------------------------------------------------//
// 请求2:关闭编辑流程页面时,调用解锁
lockManager.unlock(resourceId);

场景2:控制集群环境的用户并发操作

long resourceId;
long userId = AppContext.currentUserId();
boolean result = lockManager.check(userId,resourceId);

// 可操作
if(result){
    result = lockManager.lock(userId, resourceId);
}

if(!result){
    // 不可操作或加锁失败
    // 提示前端用户重试,也可以采取3的方式等待一段时间重试,但考虑用户体验,时间不宜过长
}else{
    try{
        doSomething();
    } catch (Exception e) {
        // ...
    }finally {
        // 完成业务操作后解锁
        lockManager.unlock(resourceId);
    }  
}

场景3:控制集群环境的后台并发操作

long resourceId;
long userId = AppContext.currentUserId();
boolean result = checkAndLock(userId,resourceId);
if(!result){
    // 不可操作或加锁失败
    // 等待重试 如果能确保不会死锁,也可以while(true)
    for (int i = 0; i < 20; i++) {
        if(!checkAndLock(userId,resourceId)){
           Thread.sleep(1000);
        }
    }
}
if(result){
    try{
        doSomething();
    } catch (Exception e) {
        // ...
    }finally {
        // 完成业务操作后解锁
        lockManager.unlock(resourceId);
    }  
}else{
    // 无法获取锁,抛出异常
}

private boolean checkAndLock(long userId,long resourceId){
    boolean result = lockManager.check(userId,resourceId);
    // 可操作
    if(result){
        result = lockManager.lock(userId, resourceId);
    }
    return result;
}

# 无效场景案例

锁存在所属人的属性,而且对应所属人的在线状态也有很严格的要求,因此对于后台定时任务此类无法获取到当前登录人员的情况下,使用这套锁逻辑无效。 例如所属人传递的是协同发起人,对于同一个协同的两个用户场景,执行到此逻辑的情况下,如果协同发起人处于离线状态,那么第一次运行到判断逻辑,会校验未锁定,然后加锁。第二次运行到判断逻辑,会校验锁属于自己(实际情况是其他人锁的),继续执行代码逻辑。最终导致出现重入现象。

创建人:het
修改人:het