前篇:Redis+Lua实现分布式锁
场景描述:
更新一个页面信息,首先更新页面在导航中的信息,然后删除页面下所有图表的组件以及配置等详细信息并保存新页面的所有图表信息。
注:下列方法存在于在前篇:Redis+Lua实现分布式锁
类:LockSerice.java
1.方法lock(String lock, int expire) :获取锁
2.方法softUnlock(String lock):解锁,只有当锁过期时才会解锁
3.方法hardUnlock(String lock):解锁,不关心锁的是否过期
4.方法getLockStatus(String lock):获取锁的状态
5.方法autoIncrLock(String lock):锁的值自加1,如果锁本身不存在,默认从0开始
6.方法autoDecrLock(String lock):锁的值自减1,如果锁本身不存在,默认从0开始
7.方法softUnlockAuto(String lock):解锁,只有当锁过期时才会解锁
8.方法hardUnlockAuto(String lock):解锁,不关心当前锁的状态
9.方法getLockStatusAuto(String lock):获取锁的状态
说明:下面提到的对线程的锁和页面的锁使用了相同的lock,但是在放入缓存前会加前缀,所有,尽管线程的锁和页面的锁显示的lock一样,但是实际上不同,后续不在对此做赘述。以下代码只是上述问题的逻辑展现,没有实现细节以及异常问题处理,参考时请注意!
一、getPageInfo( Long pageId)
获取页面信息:现获取页面锁,然后再获取线程锁(若拿不到稍微等一会),如果实在等不到(很可能已经挂了),就从缓存中读取备份的数据。
PageInfo getPageInfo ( Long pageId) throws Exception {
Assert.isTrue(getLockForWait(lock, 2), "页面正在更新,请稍后重试...");
PageInfo pageInfo;
try {
if(!getThreadLockForWait(lock, 2)){
pageInfo = ##从Redis中获取备份的数据##;
} else {
pageInfo = ##从数据库中获取数据##;
}
} finally {
lockService.hardUnlock(lock);
}
return pageInfo;
}
二、getLockForWait(String lock, int expire)
获取页面的锁,如果拿不到锁,就稍微等一会,如果实在等不到就算了。
private boolean getLockForWait(String lock, int expire) throws Exception {
boolean status;
int count =5 * expire;
while (!(status = getLock(lock)) && count >0) {
Thread.sleep(200);
count--;
}
return status;
}
三、getThreadLockForWait(String threadLock, int expire)
获取线程的锁,如果拿到了,就清除上次的备份数据。返回true
如果拿不到,稍微等一会,实在等不到,强制解锁,返回false。这种情况还需要在进步一处理,目前还没有想好方案。
public boolean getThreadLockForWait(String threadLock, int expire) throws Exception {
int count =5 * expire;
while (!lockService.getLockStatusAuto(threadLock) && count >0) {
count--;
Thread.sleep(200);
}
if (lockService.softUnlockAuto(threadLock)) {
##do:删除缓存中备份的数据##
return true;
} else {
lockService.hardUnlockAuto(lock);
return false;
}
}
三、updatePageInfo(PageInfo pageInfo)
更新页面,先获取页面的锁,拿到之后,再判断之前保存该页面时的线程是否已经执行完成,如果没有执行完就稍微等一会,如果实在等不到,则在缓存在备份一份数据。
Long updatePageInfo(PageInfo pageInfo) throws Exception {
Long pageId = pageInfo.getId();
if(lockService.lock(lock, expire)) {
updateMenuInfo(pageInfo);
deletePageInfo(pageId);//删除页面配置必须放在更新导航之后,如有疑问可留言
updatePageInfo(pageId, pageInfo);
tryBackups(lock, pageInfo);
lockService.hardUnlock(lock);
}
}
四、 tryBackups(String lock, PageInfo pageInfo)
尝试备份,如果线程执行完成则直接返回true。若线程尚未执行完,则等待几秒(这个方法可以抛出一个线程去执行),如果线程在规定等待时间内一直未能完成,则进行备份。
public void tryBackups(String lock, PageInfo pageInfo) {
int count =100;
while (!lockService.softUnlockAuto(lock) && count >0) {
count--;
Thread.sleep(200);
}
if(!lockService.softUnlockAuto(lock)) {
do:在缓存中做备份,可设定过期时间为1天
}
}
五、deletePageInfo(final Long pageId)
删除页面,以多线程的方式删除,并记录线程的状态
void deletePageInfo(final Long pageId) throws Exception {
final String lock = String.valueOf(pageId);
List
for (ChartInfo chartInfo : chartInfos) {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
autoIncrLock(lock);
deleteChartInfo(pageId);
autoDecrLock(lock);
}}}}
deletePageInfo(pageId);
}
六、updatePageInfo(final Long pageId, PageInfo pageInfo)
保存页面中的图表信息,以多线程的方式执行,并记录线程的状态
void updatePageInfo(final Long pageId, PageInfo pageInfo) throws Exception{
if(pageId != null){
final String lock = String.valueOf(pageId);
if(CollectionUtils.isNotEmpty(pageInfo)){
for(final CharInfo chartInfo : pageInfo){
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
autoIncrLock(lock);
updateChartInfo(pageId);
autoDecrLock(lock);
}}}}}}
问题总结:
分布式锁在使用过程中需要特别注意不同锁之前的嵌套,防止产生死锁。
多线程在使用过程中,要注意并发是否对线程执行状态有影响。
上述提到的实例中,锁的粒度一共有3种。
第一种为站点级别的锁,防止导航菜单并发带来的问题。
第二种为页面级别的锁,防止页面并发带来的问题。
第三类是线程级别的锁,主要是为了多线程的情况下,存在未完成线程带来的问题。
其中,线程的锁最容易产生问题,因此增加了简单的容错机制,当线程的锁不能释放时,做了相应的等待以及备份处理。页面更新时,页面被锁,其他任何进程均不允许修改以及获取该页面的信息,以免获取的信息不完整以及相互覆盖的问题。当页面被读取时(此处应该做一下调整,页面读取时,应该允许其他线程读取,但是不允许更新)