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