Bootstrap

spring-data-redis redis 分布式锁

概述

 1、在没有集成spring-data-redis的项目中实现分布式锁,可以借助与jedis来需要增加jedisclient相关的配置和初始化代码,而已经集成 spring-data-redis的项目可以直接使用redisTemplate.execute 来实现。

2、加锁涉及到原子性,为了保证原子性 ,加锁需要使用执行lua脚本的方式,调用redisTemplate.execute执行lua脚本。

代码实现

public class RedisLockException extends Exception{
    public RedisLockException(String message) {
        super(message);
    }

    public RedisLockException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class RedisCommand {

	public static boolean lock(RedisTemplate<String, Object> redisTemplate, String key, String id, long expire) {
		DefaultRedisScript<String> script = new DefaultRedisScript<>();
		script.setScriptText("if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1],ARGV[1], 1); " +
				"redis.call('pexpire', KEYS[1], ARGV[2]); return nil; end; " +
				"if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1); " +
				"redis.call('pexpire', KEYS[1], ARGV[2]); return nil; end; return redis.call('pttl', KEYS[1]);");
		script.setResultType(String.class);
		List<String> list = new ArrayList<>();
		list.add(key);
		String result = redisTemplate.execute(script, list, id, expire);
		//result 为null时,加锁成功
		return result == null;
	}

	public static void unlock(RedisTemplate<String, Object> redisTemplate, String key, String id) {
		DefaultRedisScript<Long> script = new DefaultRedisScript<>();
		script.setScriptText("if (redis.call('exists', KEYS[1]) == 0) then return 0; end; " +
				"if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then return 0; end; " +
				"local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
				"if (counter > 0) then return 1; " +
				"else " +
				"redis.call('del', KEYS[1]); return 1; end;");
		script.setResultType(Long.class);
		List<String> list = new ArrayList<>();
		list.add(key);
		redisTemplate.execute(script, list, id);
	}

	public static boolean isLocked(RedisTemplate<String, Object> redisTemplate, String key, String id) {
		DefaultRedisScript<Long> script = new DefaultRedisScript<>();
		script.setScriptText("if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then return 0; end; " +
				"return 1;");
		script.setResultType(Long.class);
		List<String> list = new ArrayList<>();
		list.add(key);
		Long result = redisTemplate.execute(script, list, id);
		return result == 1L;
	}
}
public interface RLock {
    /**
     * acquires the lock.
     *
     * @param expire   redis key timeout
     * @param timeUnit the time unit of the timeout argument
     */
    void lock(long expire, TimeUnit timeUnit) throws InterruptedException, RedisLockException;

    /**
     * acquires the lock if lock is free
     *
     * @param expire   redis key timeout
     * @param timeUnit the time unit of the timeout argument
     * @return
     */
    boolean tryLock(long expire, TimeUnit timeUnit) throws RedisLockException;

    /**
     * try to Acquires the lock
     *
     * @param timeout  the time to wait for the lock
     * @param expire   redis key timeout
     * @param timeUnit the time unit of the timeout argument
     * @return
     */
    boolean tryLock(long timeout, long expire, TimeUnit timeUnit) throws InterruptedException, RedisLockException;

    /**
     * check if current thread is owner
     *
     * @return
     */
    boolean isHeldByCurrentThread();

    /**
     * if any thread holds this lock
     *
     * @return
     */
    boolean isLocked() throws RedisLockException;

    void unlock() throws RedisLockException;
}

/**
 * @author zzm
 * @version V1.0
 * @date 2017-09-26 14:18
 **/
public class RedisLock implements RLock {
	private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
	private Thread thread;
	private String key;
	private UUID id = UUID.randomUUID();
	private Random random = new Random();
	private int unlockRetry;
	private RedisTemplate<String, Object> redisTemplate;
	//static final long spinForTimeoutThreshold = 1000L;

	public static RedisLock create(String key, RedisTemplate<String, Object> redisTemplate) {
		return create(key, redisTemplate, 1);
	}

	/**
	 * @param key         当前服务的别名
	 * @param unlockRetry 解锁重试次数
	 * @return
	 */
	public static RedisLock create(String key, RedisTemplate<String, Object> redisTemplate, int unlockRetry) {
		return new RedisLock(key, redisTemplate, unlockRetry);
	}

	private RedisLock(String key, RedisTemplate<String, Object> redisTemplate, int unlockRetry) {
		this.key = key;
		this.redisTemplate = redisTemplate;
		this.unlockRetry = unlockRetry;
	}

	/**
	 * 加锁
	 *
	 * @param expire   redis key timeout
	 * @param timeUnit the time unit of the timeout argument
	 * @throws InterruptedException
	 */
	@Override
	public void lock(long expire, TimeUnit timeUnit) throws InterruptedException {
		if (expire <= 0L) throw new IllegalArgumentException("expire time least gt zero");
		String field = getLockName(Thread.currentThread().getId());
		boolean result;
		for (; ; ) {
			result = RedisCommand.lock(redisTemplate, key, field, timeUnit.toMillis(expire));
			if (result) {
				thread = Thread.currentThread();
				return;
			} else {
				Thread.sleep(random.nextInt(10));
			}
		}
	}

	@Override
	public boolean tryLock(long expire, TimeUnit timeUnit) {
		String field = getLockName(Thread.currentThread().getId());
		boolean result = RedisCommand.lock(redisTemplate, key, field, timeUnit.toMillis(expire));
		if (result) {
			thread = Thread.currentThread();
			return true;
		}
		return false;
	}

	@Override
	public boolean tryLock(long timeout, long expire, TimeUnit timeUnit) throws InterruptedException {
		if (expire <= 0L) throw new IllegalArgumentException("expire time least gt zero");
		if (timeout <= 0L) throw new IllegalArgumentException("timeout time least gt zero");
		final long deadline = System.nanoTime() + timeUnit.toNanos(timeout);
		String field = getLockName(Thread.currentThread().getId());
		boolean result;
		for (; ; ) {
			result = RedisCommand.lock(redisTemplate, key, field, timeUnit.toMillis(expire));
			if (result) {
				thread = Thread.currentThread();
				return true;
			} else {
				long remaining = deadline - System.nanoTime();
				if (remaining <= 0L)
					return false;
				LockSupport.parkNanos(remaining);
			}
		}
	}

	@Override
	public boolean isHeldByCurrentThread() {
		return thread == Thread.currentThread();
	}

	@Override
	public boolean isLocked() {
		return RedisCommand.isLocked(redisTemplate, key, getLockName(Thread.currentThread().getId()));
	}

	@Override
	public void unlock() {
		if (thread != Thread.currentThread()) throw new IllegalMonitorStateException();
		String field = getLockName(Thread.currentThread().getId());
		for (int i = 0; i <= unlockRetry; i++) {
			try {
				RedisCommand.unlock(redisTemplate, key, field);
				break;
			} catch (Exception e) {
				logger.error("当前线程解锁异常,线程ID:{},error:{}", Thread.currentThread().getId(), e.getMessage());
			}
			if (unlockRetry == i) logger.warn("当前线程解锁异常,线程ID:{}", Thread.currentThread().getId());
		}
	}

	String getLockName(long threadId) {
		return this.id + ":" + threadId;
	}
}

;