分布式锁,多JVM,多线程,共享资源互斥访问
用codis,不能使用事务
极端场景:乱序,程序崩溃,网络延时等,key没有释放
http://www.redis.cn/commands/set.html
==================
package com.enterprise.util; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.exceptions.JedisException; /** * 基于REDIS的分布式锁 * * @author chen.kuan 2016年4月29日 * */ public class RedisLock { public static final int DEFAULT_AVAILABLE_TIME = 600; // 默认有效期 单位S private static final String LOCK_SUCCESS_CODE = "OK"; private JedisPool pool; private final int availableTime; private final String redisKey; public RedisLock(JedisPool pool, String redisKey) { this(pool, DEFAULT_AVAILABLE_TIME, redisKey); } /** * @param pool * @param availableTime * @param redisKey */ public RedisLock(JedisPool pool, int availableTime, String redisKey) { this.pool = pool; this.availableTime = availableTime; this.redisKey = redisKey; } /** * @return true 锁定成功 false 锁定失败 */ public boolean tryLock() { String code = lock(); if (LOCK_SUCCESS_CODE.equals(code)) { return true; } return false; } /** * 基于 set(final String key, final String value, final String nxxx, final String expx, final int time)上锁. </br> * 上锁必须设置超时时间,避免因为JVM宕机,IO等原因造成的unlock失败出现死锁问题. </br> * 如果业务执行时间过长,当指定的availableTime耗尽后,锁会被自动释放。 </br> * * * @return 状态码 */ private String lock() { Jedis jedis = null; try { jedis = pool.getResource(); String code = jedis.set(redisKey, "1", "NX", "EX", availxableTime); return code; } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { pool.returnResource(jedis); } } public void unlock() { Jedis jedis = null; try { jedis = pool.getResource(); jedis.del(redisKey); } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { if (pool != null) { pool.returnResource(jedis); } } } }
package com.enterprise.util; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.exceptions.JedisException; /** * 基于REDIS的分布式锁<br> * * @author chen.kuan 2016年4月29日 * */ public class RedisLock { public static final int DEFAULT_AVAILABLE_TIME = 600; // 默认有效期 单位S private JedisPool pool; private final int availableTime; private final String redisKey; public RedisLock(JedisPool pool, String redisKey) { this(pool, DEFAULT_AVAILABLE_TIME, redisKey); } public RedisLock(JedisPool pool, int availableTime, String redisKey) { this.pool = pool; this.availableTime = availableTime; this.redisKey = redisKey; } /** * @return true 锁定成功 false 锁定失败 */ public boolean tryLock() { Long statusCode = lockAndSetExpiredTime(); if (statusCode == 1) { return true; } // 锁定失败 // 需要检查是是否发生了锁超时,锁超时是指锁没有被显示的释放,也就是REDIS中的键值对没有被删掉 String expectedExpiredTime = get(); if (expectedExpiredTime != null && System.currentTimeMillis() > Long.valueOf(expectedExpiredTime)) { // 锁超时: 多JVM,多线程,时间一致 // 超时可能是下面的原因导致: // 1 代码设计疏忽,获得了lock后,在某些场景下不会执行unlock // 2 持有lock的线程,业务执行时间过长 --> 这是最糟糕的情况,因为它的unlock可能再未来的某一个时刻突然执行了 // 3 持有lock的线程,执行unlock失败,REDIS connection fail可能会导致这种情况,分布式环境下必须考虑某一个REDIS客户端访问REDIS失败的问题 // 4 进程还没有来得及执行unlock就直接被kill -9杀死了 // 检测到了锁超时,需要释放锁,保证后续的业务能够锁定成功 // 这个地方只能通过设置超时的方式保证锁被删除,而且超时时间必须为availableTime // 原因如下: // 1 在本线程尝试删除锁时,REDIS锁可能已经被删除了(unlock得到了执行,或者其它线程设置的setExired发挥作用了),此时其它线程可能已经获取了锁 // 2 如果其它线程获取了锁,则我们必须保证竞争机制能够延续,所以要设置有效期和availableTime一致 if (!expired()) { setExpired(); } } return false; } private Long lockAndSetExpiredTime() { Jedis jedis = null; try { jedis = pool.getResource(); Long reV = jedis.setnx(redisKey, (System.currentTimeMillis() + availableTime * 1000) + ""); return reV; } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { pool.returnResource(jedis); } } private String get() { Jedis jedis = null; try { jedis = pool.getResource(); String value = jedis.get(redisKey); return value; } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { if (pool != null) { pool.returnResource(jedis); } } } private boolean expired() { Jedis jedis = null; try { jedis = pool.getResource(); Long ttl = jedis.pttl(redisKey); // 当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以毫秒为单位,返回 key 的剩余生存时间 if (ttl != null && ttl >= 0) { return true; } return false; } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { if (pool != null) { pool.returnResource(jedis); } } } private void setExpired() { Jedis jedis = null; try { jedis = pool.getResource(); jedis.expire(redisKey, availableTime); } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { if (pool != null) { pool.returnResource(jedis); } } } /** * 释放锁,持有锁的线程在业务操作完毕后,必须调用此方法释放持有的锁 */ public void unlock() { Jedis jedis = null; try { jedis = pool.getResource(); jedis.del(redisKey); } catch (Exception e) { if (jedis != null) { pool.returnBrokenResource(jedis); } throw new JedisException(e); } finally { if (pool != null) { pool.returnResource(jedis); } } } }
package com.enterprise.util; /** * 基于ReidsLock,实现对业务实体的并发互斥访问 * * @author chen.kuan 2015年11月13日 * */ public abstract class ConcurrentEntityVisitor { private RedisLock lock; public ConcurrentEntityVisitor() { } public ConcurrentEntityVisitor(RedisLock lock) { this.lock = lock; } public void setLock(RedisLock lock) { this.lock = lock; } public Result visit() { boolean locked = lock.tryLock(); if (!locked) { return handleConflict(); } else { try { return doVisit(); } finally { lock.unlock(); } } } /** * 并发场景下,在检测到访问冲突时处理冲突 * * @return */ public abstract Result handleConflict(); /** * 访问实体,做业务 * * @return */ public abstract Result doVisit(); }
private RedisLock createRedisLock(String merchantUsername) { String key = String.format(O2O_MERCHANT_CONCURRENT_LOCK_KEY, merchantUsername); RedisLock lock = new RedisLock(redisUtil.getPool(), key); return lock; } @Override @Transactional(value = "transactionManager_busi", rollbackFor = Exception.class) public Result addScanCodePayMerchant(final String username, final String auditUserName) { ConcurrentEntityVisitor visit = new ConcurrentEntityVisitor(createRedisLock(username)) { @Override public Result handleConflict() { return Result.failed("当前商家" + username + "正在被操作"); } @Override public Result doVisit() { MerchantO2OConfig config = repository.getMerchantO2OConfig(username, null); if (config != null) { return Result.failed("当前商家" + username + "已经存在于扫码支付列表中"); } MerchantEntity merchantEntity = merchantBasicService.getMerchantByUserName(username); if (merchantEntity == null) { return Result.failed("当前商家" + username + "不存在"); } String shopName = merchantEntity.getShopName(); config = new MerchantO2OConfig(); config.setAuditUserName(auditUserName); config.setCreateTime(new Date()); config.setMerchantType(merchantEntity.getMerchantType()); config.setShopName(shopName); config.setState(MerchantO2OConstants.SCAN_CODE_PAY_GRANTED); // 状态设置为已赋权 config.setUpdateTime(new Date()); config.setUsername(username); config.setUserId(merchantEntity.getUserId()); repository.addScanCodePayMerchant(config); supportUsingRedis(username); return Result.success(); } }; return visit.visit(); }