相关转载:http://blog.csdn.net/ugg/article/details/41894947
大致的思路先赋上流程图:
上图是加锁的流程图,解锁相对简单,就不赋流程图了
了解了方法之后就在本地码代码;调试,心情也是蛮激动的,哈哈。
我的大致策略是:
1.加锁的方法lock会传一个key,这个key唯一标识一个待处理的独立个体,比如一个用户;一笔借款等,接下来对这个独立个体的一些敏感操作中,加锁对应的key永远是唯一的,这个很重要,否则redis锁也无从谈起了。
2.lock方法如果竞争锁成功则会返回set到redis中这个key的时间戳即加锁的时间,我的理解是这样返回比返回布尔值有个好处:
1)我们在获得锁之后,为了使这个锁能够更加健壮,应该在调用关键业务的时候重新从redis中获取最新的时间戳,与写入的时间戳作比较,确保这个锁没有被其他线程del掉。
2)我们在处理完逻辑代码在解锁(releaseLock方法)的时候可以回传这个时间戳,与redis中缓存的时间戳比较,只有符合条件(看代码注释)方可解锁,这样做可以在很大程度上解决一个线程的独占锁被别的线程误删的情况。
3.为了让这个锁更加强壮,获取锁的客户端,应该在调用关键业务的时候再次从redis中获取最新的时间戳,与写入的时间戳比较,以免锁因为其他情况被执行DEL意外解开而不知。
赋上代码:
@Service public class LockServiceImpl implements LockService { private static final Long FETCH_LOCK_TIMEOUT = 1000 * 3L; public final static Integer REDIS_DATABASE_LOCK = 3; protected Logger logger = LogManager.getLogger(this.getClass()); @Override public Long lock(String key) { int retry = 3;//重试3次 Jedis jedis = null; Long nowTime; try { jedis = JedisUtils.getJedis(REDIS_DATABASE_LOCK); while (retry > 0) { nowTime = System.currentTimeMillis(); Long result = jedis.setnx(key, nowTime+""); if (result == 1)//获取到锁 return nowTime; //未获取到锁,判断旧锁是否超时 30s String lastTimeStr = jedis.get(key); if (lastTimeStr == null){ retry--; logger.error("旧锁已释放,去竞争 retry "+retry+":" + Thread.currentThread().getName()); continue;//旧锁已释放,去竞争 } Long lastTime = Long.valueOf(lastTimeStr); Long interval = nowTime - lastTime; if (interval.longValue() < FETCH_LOCK_TIMEOUT.longValue()){ Thread.sleep(FETCH_LOCK_TIMEOUT/15);//旧锁未超时,睡2s再竞争 retry--; logger.error("旧锁未超时,睡0.2s再竞争 retry "+retry+":" + Thread.currentThread().getName()); continue; } //旧锁已超时,竞争新锁,同时返回旧锁时间 String oldTimeStr = jedis.getSet(key,nowTime+""); if (oldTimeStr != null && Long.valueOf(oldTimeStr).longValue() != lastTime.longValue()){ Thread.sleep(FETCH_LOCK_TIMEOUT/15)//存在另外的线程先一步执行了以上操作,睡2s再竞争 retry--; logger.error("存在另外的线程先一步执行了以上操作,睡0.2s再竞争 retry "+retry+":" + Thread.currentThread().getName()); continue; } // 两个时间一致,说明不存在另外的并发线程竞争到锁,竞争锁成功 return nowTime; } return null; } catch (Exception e) { e.printStackTrace(); throw new PledgeException("账户锁加锁异常!"); } finally { if(jedis != null){ logger.debug("Close jedis connection and quit key:"+key); JedisUtils.returnResource(REDIS_DATABASE_LOCK, jedis); } } } @Override public ResultCode releaseLock(String key,Long lockedTime) { if (lockedTime == null) return ResultCode.RELEASE_NOLOCK; Jedis jedis = null; try { jedis = JedisUtils.getJedis(REDIS_DATABASE_LOCK); Long lastTime = Long.valueOf(jedis.get(key)); if (lockedTime.longValue() >= lastTime.longValue()){ jedis.del(key); //如果redis中的加锁时间比本次处理单元中的加锁时间大,说明当前锁是当前线程之后的线程加进去的,不进行del操作 return ResultCode.RELEASE_SUCCESS; } return ResultCode.RELEASE_DONE; } catch (Exception e) { e.printStackTrace(); return ResultCode.RELEASE_ERROR; } finally { if(jedis != null){ logger.debug("Close jedis connection and quit key:"+key); JedisUtils.returnResource(REDIS_DATABASE_LOCK, jedis); } } } }
package com.htt.app.pledge.service.lock; /** * 解锁结果枚举 * Created by sunnyLu on 2017/05/10. */ public enum ResultCode { RELEASE_SUCCESS("锁释放成功",1), RELEASE_NOLOCK("没有竞争到锁",2), RELEASE_DONE("锁已被其他线程释放",3), RELEASE_ERROR("锁释放异常",4); /** * 创建一个新的实例 ResultCode. * * @param categoryId * @param des */ private ResultCode(String des, int categoryId) { this.des = des; this.categoryId = categoryId; } private String des; private int categoryId; public String getDes() { return des; } public int getCategoryId() { return categoryId; } public static ResultCode findByCategoryId(int categoryId){ for(ResultCode type : values()){ if(type.getCategoryId() == categoryId){ return type; } } return null; } }
本地模拟了多个线程并发访问的情况:
package com.htt.app.pledge.service.lock; import org.apache.log4j.Logger; import org.springframework.beans.BeansException; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * Created by sunnyLu on 2017/5/10. */ public class LockThread extends Thread{ private LockService lockService; private Logger logger = Logger.getLogger(LockThread.class); public LockThread() { } public LockThread(LockService lockService) { this.lockService = lockService; } @Override public void run() { String key = "test_lock"; Long lockTime = null; try { lockTime = lockService.lock(key); if (lockTime != null){ logger.error("竞争锁成功:" + Thread.currentThread().getName()); logger.error("处理逻辑:" + Thread.currentThread().getName()); logger.error("模拟业务逻辑处理超时或者程序异常情况睡3秒:" + Thread.currentThread().getName()); Thread.sleep(3000); } else { logger.error("竞争锁失败:" + Thread.currentThread().getName()); } } catch (Exception e) { e.printStackTrace(); } finally { ResultCode result = lockService.releaseLock(key,lockTime); if (!result.equals(ResultCode.RELEASE_NOLOCK)){ logger.error(result.getDes()+":"+Thread.currentThread().getName()); } } } public static void main(String args[]) { ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/beans/**/*.xml"); try { LockService lockService = applicationContext.getBean(LockService.class); for (int i = 0;i<5;i++){ LockThread thread = new LockThread(lockService); thread.start(); } } catch (BeansException e) { e.printStackTrace(); } } }
控制台打印结果:
Thread-8竞争到了锁并且花了3s时间处理,在这个过程中其他的线程分别尝试了3次都没有能够获取到锁,只能等到Thread-8释放锁。
接下来模拟另外一种场景,线程1竞争到锁但是由于客观因素造成死锁,其他线程尝试去竞争锁...
我们将默认的锁过期时间设成2s,而竞争到锁的线程依然sleep 3s,每次尝试重新竞争锁都先sleep 1s,以此来模拟死锁的情况
控制台打印结果:
同样是Thread-8竞争到锁,其他线程前两次尝试获取锁都失败,第三次尝试时,Thread-8的锁已过期,Thread-11竞争锁成功,最后Thread-8尝试解锁发现自己的锁已被释放(如果是死锁的情况Thread-8并不会去尝试解锁),而Thread-11能够成功解锁。
最后在调试中发现,释放锁时还存在一个问题:
1.线程1竞争到锁,但是程序异常崩溃造成死锁。
2.线程2,线程3先后去竞争锁,发现线程1的锁已过期,并且先后通过getset方法尝试获取锁
3.假设线程2先一步获取到锁,那么线程3getset,覆盖了线程2getset到redis中的时间戳
上面这种情况,虽然从实际情况来看线程2依旧独占锁,但是当线程2处理完逻辑而去释放锁时,会发现redis中的时间戳大于自己set进去的时间戳,而误以为是并发原因造成了锁已被另外的线程占用,不去del,而事实上这个锁是线程3竞争失败的无效锁。
思来想去没有想到真正完善的解决方案,所以这个锁并不能严格意义上解决所有的并发问题,在高并发的情况下可能会出现存在无效锁而加锁失败的情况,或多或少会影响用户体验,不过相比较高并发造成的数据不同步问题来说这个影响还是可以承受的。我们的锁有过期失效机制,只要能够合理的设计失效时间,做好一定的容错,还是能够满足应用的需求。