最近项目中使用到了redis实现的分布式锁,自定义的分布式锁支持自旋和可冲入等,是一个不错的实践,这里记录下
/** * @description: redis分布式锁 * @author: cc.wang * @createDate: 2022-04-22 12:13 * @version: 1.0 */ public class RedisLock { private static Logger logger = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate<String, Object> redisTemplate; private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100; /** * Lock key path. */ private String lockKey; /** * 锁超时时间,防止线程在入锁以后,无限的执行等待 */ private int expireMsecs = 3000; /** * 锁等待时间,防止线程饥饿 */ private int timeoutMsecs = 100; private volatile boolean locked = false; private String requestClientId = ""; protected static final String UNLOCK_LUA; static { StringBuilder sb = new StringBuilder(); sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] "); sb.append("then "); sb.append(" return redis.call(\"del\",KEYS[1]) "); sb.append("else "); sb.append(" return 0 "); sb.append("end "); UNLOCK_LUA = sb.toString(); } /** * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs. * * @param lockKey lock key (ex. account:1, ...) */ public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * Detailed constructor with default lock expiration of 60000 msecs. */ public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int timeoutMsecs) { this(redisTemplate, lockKey); this.timeoutMsecs = timeoutMsecs; } /** * Detailed constructor. */ public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) { this(redisTemplate, lockKey, timeoutMsecs); this.expireMsecs = expireMsecs; } /** * @return lock key */ public String getLockKey() { return this.lockKey; } private String get(final String key) { try { RedisCallback<String> callback = (connection) -> { JedisCommands commands = (JedisCommands) connection.getNativeConnection(); return commands.get(key); }; String result = redisTemplate.execute(callback); return result; } catch (Exception e) { logger.error("获取锁内容异常", e); } return ""; } private boolean setNX(final String key, final String value) { try { RedisCallback<String> callback = (connection) -> { JedisCommands commands = (JedisCommands) connection.getNativeConnection(); return commands.set(lockKey, value, "NX", "PX", this.expireMsecs); }; String result = redisTemplate.execute(callback); logger.debug("locked result: {}", result); return StringUtils.isNotBlank(result); } catch (Exception e) { logger.error("加锁失败", e); } return false; } /** * @return true if lock is acquired, false acquire timeouted * @throws InterruptedException in case of thread interruption */ public synchronized boolean lock() throws InterruptedException { int timeout = timeoutMsecs; if (StringUtils.isBlank(this.requestClientId)) { requestClientId = UUID.randomUUID().toString(); logger.debug("redis lock requestClientId: {}", requestClientId); } //支持自旋 while (timeout >= 0) { boolean isLocked = this.setNX(this.lockKey, this.requestClientId); if (isLocked) { this.locked = isLocked; return true; } if (this.locked) { //支持重入 String value = this.get(this.lockKey); logger.debug("locked requestClientId: {}", value); if (StringUtils.equalsIgnoreCase(this.requestClientId, value)) { return true; } } timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS; if (timeout > 0) { doSleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS); } } return false; } /** * 休眠一段时间 * * @param milliseconds * @throws InterruptedException */ private void doSleep(long milliseconds) throws InterruptedException { Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS); } /** * Acqurired lock release. */ public synchronized boolean unlock() { if (!this.locked) { //未取得锁时直接返回 return false; } try { List<String> keys = Lists.newArrayList(this.lockKey); List<String> args = Lists.newArrayList(this.requestClientId); // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本 RedisCallback<Long> callback = (connection) -> { Object nativeConnection = connection.getNativeConnection(); // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 if (nativeConnection instanceof JedisCluster) { // 集群模式 return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args); } else if (nativeConnection instanceof Jedis) { // 单机模式 return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args); } return 0L; }; Long result = redisTemplate.execute(callback); boolean reply = result != null && result > 0; if (reply) { this.locked = false; } return reply; } catch (Exception e) { logger.error("release lock occured an exception", e); } return false; } }