# 应用锁
应用锁组件用于控制并发修改,比如协同流程并发修改和公文正文并发编辑控制。
为应用提供统一的独占锁,管理用户、资源、操作三者之间的关系。 只控制锁的状态(加锁、解锁、锁失效、锁判断),不实际对资源进行锁定,资源和操作的锁定由应用自己管理。
为同时适应单机和集群环境,提升性能,提供内存锁和数据库锁两种模式。 单机环境使用内存保存锁;集群环境使用数据库保存锁。
# 功能特性
功能一: 锁定资源 特定用户锁定资源,解锁或锁失效之前其他用户不允许访问资源。 锁定资源后指定资源只允许加锁者独占使用。
功能二: 锁定资源的操作 特定用户锁定资源的操作,解锁或锁失效之前其他用户不允许进行资源的操作。
功能三: 解锁 解除资源的所有锁或解除资源特定操作的锁。
功能四: 锁判断 判断指定用户是否可以访问资源或资源的操作。 这里会有一定歧义:对资源加锁是锁定了资源的所有操作,还是操作为空本身就是一种操作。 在这里不做处理,由应用自己解释。如果有特定的解释,请不要使用组件提供的锁判断方法,取得资源的所有锁后自己判断。
功能五: 锁过期 提供了锁过期的策略,缺省过期时间设置为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;
}
# 无效场景案例
锁存在所属人的属性,而且对应所属人的在线状态也有很严格的要求,因此对于后台定时任务此类无法获取到当前登录人员的情况下,使用这套锁逻辑无效。 例如所属人传递的是协同发起人,对于同一个协同的两个用户场景,执行到此逻辑的情况下,如果协同发起人处于离线状态,那么第一次运行到判断逻辑,会校验未锁定,然后加锁。第二次运行到判断逻辑,会校验锁属于自己(实际情况是其他人锁的),继续执行代码逻辑。最终导致出现重入现象。