Distributed locks, multiple JVMs, multiple threads, mutually exclusive access to shared resources
With codis, transactions cannot be used
Extreme scenarios: out of order, program crash, network delay, etc., the key is not released
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; /** * Distributed lock based on REDIS * * @author chen.kuan April 29, 2016 * */ public class RedisLock { public static final int DEFAULT_AVAILABLE_TIME = 600; // Default validity unit 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 lock success false lock failure */ public boolean tryLock() { String code = lock(); if (LOCK_SUCCESS_CODE.equals(code)) { return true; } return false; } /** * Lock based on set(final String key, final String value, final String nxxx, final String expx, final int time). </br> * A timeout must be set for locking to avoid deadlock problems caused by unlock failures due to JVM downtime, IO and other reasons. </br> * If the business execution time is too long, the lock will be automatically released when the specified availableTime is exhausted. </br> * * * @return status code */ 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; /** * Distributed lock based on REDIS<br> * * @author chen.kuan April 29, 2016 * */ public class RedisLock { public static final int DEFAULT_AVAILABLE_TIME = 600; // Default validity unit 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 lock success false lock failure */ public boolean tryLock() { Long statusCode = lockAndSetExpiredTime (); if (statusCode == 1) { return true; } // lock failed // Need to check whether the lock timeout has occurred. The lock timeout means that the lock is not displayed and released, that is, the key-value pair in REDIS has not been deleted. String expectedExpiredTime = get(); if (expectedExpiredTime != null && System.currentTimeMillis() > Long.valueOf(expectedExpiredTime)) { // Lock timeout: multiple JVMs, multiple threads, consistent time // Timeouts can be caused by: // 1 The code design is negligent. After the lock is obtained, the unlock will not be executed in some scenarios. // 2 The thread holding the lock, the business execution time is too long --> This is the worst case, because its unlock may be suddenly executed at some point in the future // 3 If the thread holding the lock fails to execute the unlock, the REDIS connection fail may cause this situation. In a distributed environment, the problem that a REDIS client fails to access REDIS must be considered. // 4 The process was killed by kill -9 before it had time to execute unlock // The lock timeout is detected, and the lock needs to be released to ensure that subsequent services can be locked successfully // This place can only ensure that the lock is deleted by setting a timeout, and the timeout must be availableTime // The reason is as follows: // 1 When this thread tries to delete the lock, the REDIS lock may have been deleted (unlock has been executed, or the setExired set by other threads has played a role), and other threads may have acquired the lock at this time // 2 If other threads acquire the lock, we must ensure that the competition mechanism can continue, so set the validity period to be consistent with 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); // When the key does not exist, return -2. Returns -1 when the key exists but no remaining time to live is set. Otherwise, in milliseconds, returns the remaining time to live for the 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); } } } /** * Release the lock, the thread holding the lock must call this method to release the lock after the business operation is completed */ 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; /** * Based on ReidsLock, realize concurrent and mutually exclusive access to business entities * * @author chen.kuan November 13, 2015 * */ 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(); } } } /** * In concurrent scenarios, handle conflicts when access violations are detected * * @return */ public abstract Result handleConflict(); /** * Access entities and do business * * @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("current business" + username + "being operated"); } @Override public Result doVisit() { MerchantO2OConfig config = repository.getMerchantO2OConfig(username, null); if (config != null) { return Result.failed("Current merchant" + username + "Already exist in the scan code payment list"); } MerchantEntity merchantEntity = merchantBasicService.getMerchantByUserName(username); if (merchantEntity == null) { return Result.failed("Current business" + username + "does not exist"); } 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); // state is set to Granted config.setUpdateTime(new Date()); config.setUsername(username); config.setUserId(merchantEntity.getUserId()); repository.addScanCodePayMerchant(config); supportUsingRedis(username); return Result.success(); } }; return visit.visit(); }