一 设计思路
1. 本地锁:本地多线程并发时,保证只有一个线程在竞争 redis 锁;
2. redis 锁(setNX):分布式部署多个服务器之间并发时,保证只有一个服务器可以竞争到 redis 锁;
3. 本地多线程并发时,先获取本地锁成功,然后再去尝试获取 redis 锁;
redis 锁的实现原理 setNX:redis 的 setNX 操作在设置同一个 key 时,只会有一个成功,当已经存在了该 key 时,setNX 接口会返回 false,也就可以理解为获取锁失败。setNX 成功的即可理解为获取 redis 锁成功,再设置一个超时时间,防止发生异常时死锁。
二 实现类
package com.qbian.common.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Qiangqiang.Bian
* @create 18/8/27
* @desc redis 实现的分布式锁
**/
@Service
@Slf4j
public class RedisLock {
@Resource
private StringRedisTemplate stringRedisTemplate;
private final long sleepTimeNanos = 100;
// 是否存在本地锁
private volatile Boolean hasLocalLock = Boolean.TRUE;
// 本地锁
private final ReentrantLock lock = new ReentrantLock(true);
private final Condition hasLock = lock.newCondition();
// 加锁超时时间, 单位 /秒
private static final long LOCK_TIMEOUT = 10;
/**
* 持续获取 redis 锁
* @param redisLockKey redis lock key
* @throws InterruptedException 等待时被中断
*/
public void lock(String redisLockKey) throws InterruptedException {
final ReentrantLock lock = this.lock;
// 本地锁已被其它线程占用,等待本地锁释放
while (!tryLocalLock()) {
lock.lockInterruptibly();
try {
hasLock.await();
} finally {
lock.unlock();
}
}
// 当前线程获取到本地锁,尝试获取 redis 锁
while (!tryRedisLock(redisLockKey))
Thread.sleep(sleepTimeNanos);
}
/**
* 尝试获取 redis 锁
* @param redisLockKey redis lock key
* @return 是否获取成功
* @throws InterruptedException 等待时被中断
*/
public boolean tryLock(String redisLockKey) throws InterruptedException {
// 没有本地锁
if (!tryLocalLock())
return false;
// 存在本地锁,尝试获取 redis 锁
if (!tryRedisLock(redisLockKey)) {
// 获取 redis 锁失败,释放本地锁
releaseLocalLock();
return false;
}
// 获取 redis 锁成功
return true;
}
/**
* 在超时时间内尝试获取 redis 锁
* @param redisLockKey redis lock key
* @param timeoutMillis 超时时长,毫秒
* @return 是否获取成功
* @throws InterruptedException 等待时被中断
*/
public boolean tryLock(String redisLockKey, long timeoutMillis) throws InterruptedException {
long startTimeMills = System.currentTimeMillis();
long nanos = TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
final ReentrantLock lock = this.lock;
// 没有本地锁,获取本地锁
while (!tryLocalLock()) {
if (nanos <= 0) {
log.info("thread({}) acquire local lock timeout, return .", Thread.currentThread().getName());
return false;
}
lock.lockInterruptibly();
try {
nanos = hasLock.awaitNanos(nanos);
} finally {
lock.unlock();
}
}
// 存在本地锁,获取 redis 锁
while (!tryRedisLock(redisLockKey)) {
Thread.sleep(sleepTimeNanos);
// 获取 redis 锁超时,释放本地锁
if (System.currentTimeMillis() - startTimeMills >= timeoutMillis) {
releaseLocalLock();
log.info("thread({}) acquire redis lock timeout, return .", Thread.currentThread().getName());
return false;
}
}
return true;
}
/**
* 释放 redis 锁
* @param redisLockKey redis lock key
* @throws InterruptedException 等待时被中断
*/
public void unlock(String redisLockKey) throws InterruptedException {
stringRedisTemplate.delete(redisLockKey);
log.info("thread({}) release redis lock .", Thread.currentThread().getName());
releaseLocalLock();
}
/**
* -----------------------------------------------------------------------------------
* -----------------------------------------------------------------------------------
*/
/**
* 释放本地锁
* @throws InterruptedException 等待时被中断
*/
private void releaseLocalLock() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
hasLocalLock = Boolean.TRUE;
hasLock.signal();
log.info("thread({}) release local lock .", Thread.currentThread().getName());
} finally {
lock.unlock();
}
}
/**
* 尝试获取本地锁
* @return 获取锁是否成功
* @throws InterruptedException 等待时被中断
*/
private boolean tryLocalLock() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
if (hasLocalLock) {
log.info("thread({}) get the local lock successfully .", Thread.currentThread().getName());
hasLocalLock = Boolean.FALSE;
return true;
}
log.info("thread({}) failed to acquire local lock .", Thread.currentThread().getName());
return false;
} finally {
lock.unlock();
}
}
/**
* 尝试获取 redis 锁
* @param redisLockKey redis lock key
* @return 获取锁是否成功
*/
private boolean tryRedisLock(String redisLockKey) {
String threadName = Thread.currentThread().getName();
if (stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(redisLockKey.getBytes(), (redisLockKey + "-" + Thread.currentThread().getName()).getBytes());
}
})) {
// 设置加锁超时时长
stringRedisTemplate.expire(redisLockKey, LOCK_TIMEOUT, TimeUnit.SECONDS);
log.info("thread({}) get the redis lock successfully .", threadName);
return true;
}
log.info("thread({}) failed to acquire redis lock, waiting .", threadName);
return false;
}
}
三 测试方法
package com.qbian.redis;
import com.qbian.BaseTest;
import com.qbian.common.common.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import javax.annotation.Resource;
/**
* @author Qiangqiang.Bian
* @create 18/8/27
* @desc
**/
@Slf4j
public class RedisLockTest extends BaseTest {
@Resource
private RedisLock redisLock;
private static final String LOCK_KEY = "java:demo:lock:test";
private static int j = 0;
@Test
public void redisLockTest() {
for (int i = 0; i < 10; ++ i) {
new Thread(() -> {
try {
if (redisLock.tryLock(LOCK_KEY, 1000)) {
try {
log.info("----------> j={}", ++ j);
} finally {
try {
redisLock.unlock(LOCK_KEY);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (InterruptedException e) {
log.error("等待时被中断,e={}", e);
}
}, "job-" + i).start();
}
}
}
四 测试结果
2018-08-27 20:56:16.346 18004 [job-0] INFO com.qbian.common.common.RedisLock - thread(job-0) get the local lock successfully .
2018-08-27 20:56:16.347 18005 [job-1] INFO com.qbian.common.common.RedisLock - thread(job-1) failed to acquire local lock .
2018-08-27 20:56:16.348 18006 [job-2] INFO com.qbian.common.common.RedisLock - thread(job-2) failed to acquire local lock .
2018-08-27 20:56:16.354 18012 [job-3] INFO com.qbian.common.common.RedisLock - thread(job-3) failed to acquire local lock .
2018-08-27 20:56:16.355 18013 [job-4] INFO com.qbian.common.common.RedisLock - thread(job-4) failed to acquire local lock .
2018-08-27 20:56:16.356 18014 [job-5] INFO com.qbian.common.common.RedisLock - thread(job-5) failed to acquire local lock .
2018-08-27 20:56:16.358 18016 [job-6] INFO com.qbian.common.common.RedisLock - thread(job-6) failed to acquire local lock .
2018-08-27 20:56:16.361 18019 [job-7] INFO com.qbian.common.common.RedisLock - thread(job-7) failed to acquire local lock .
2018-08-27 20:56:16.361 18019 [job-8] INFO com.qbian.common.common.RedisLock - thread(job-8) failed to acquire local lock .
2018-08-27 20:56:16.363 18021 [job-9] INFO com.qbian.common.common.RedisLock - thread(job-9) failed to acquire local lock .
2018-08-27 20:56:16.535 18193 [job-0] INFO com.qbian.common.common.RedisLock - thread(job-0) get the redis lock successfully .
2018-08-27 20:56:16.535 18193 [job-0] INFO com.qbian.redis.RedisLockTest - ----------> j=1
2018-08-27 20:56:16.537 18195 [job-0] INFO com.qbian.common.common.RedisLock - thread(job-0) release redis lock .
2018-08-27 20:56:16.537 18195 [job-0] INFO com.qbian.common.common.RedisLock - thread(job-0) release local lock .
2018-08-27 20:56:16.537 18195 [job-1] INFO com.qbian.common.common.RedisLock - thread(job-1) get the local lock successfully .
2018-08-27 20:56:16.539 18197 [job-1] INFO com.qbian.common.common.RedisLock - thread(job-1) get the redis lock successfully .
2018-08-27 20:56:16.539 18197 [job-1] INFO com.qbian.redis.RedisLockTest - ----------> j=2
2018-08-27 20:56:16.540 18198 [job-1] INFO com.qbian.common.common.RedisLock - thread(job-1) release redis lock .
2018-08-27 20:56:16.540 18198 [job-1] INFO com.qbian.common.common.RedisLock - thread(job-1) release local lock .
2018-08-27 20:56:16.541 18199 [job-2] INFO com.qbian.common.common.RedisLock - thread(job-2) get the local lock successfully .
2018-08-27 20:56:16.543 18201 [job-2] INFO com.qbian.common.common.RedisLock - thread(job-2) get the redis lock successfully .
2018-08-27 20:56:16.543 18201 [job-2] INFO com.qbian.redis.RedisLockTest - ----------> j=3
2018-08-27 20:56:16.544 18202 [job-2] INFO com.qbian.common.common.RedisLock - thread(job-2) release redis lock .
2018-08-27 20:56:16.544 18202 [job-2] INFO com.qbian.common.common.RedisLock - thread(job-2) release local lock .
2018-08-27 20:56:16.544 18202 [job-3] INFO com.qbian.common.common.RedisLock - thread(job-3) get the local lock successfully .
2018-08-27 20:56:16.546 18204 [job-3] INFO com.qbian.common.common.RedisLock - thread(job-3) get the redis lock successfully .
2018-08-27 20:56:16.546 18204 [job-3] INFO com.qbian.redis.RedisLockTest - ----------> j=4
2018-08-27 20:56:16.547 18205 [job-3] INFO com.qbian.common.common.RedisLock - thread(job-3) release redis lock .
2018-08-27 20:56:16.547 18205 [job-3] INFO com.qbian.common.common.RedisLock - thread(job-3) release local lock .
2018-08-27 20:56:16.548 18206 [job-4] INFO com.qbian.common.common.RedisLock - thread(job-4) get the local lock successfully .
2018-08-27 20:56:16.550 18208 [job-4] INFO com.qbian.common.common.RedisLock - thread(job-4) get the redis lock successfully .
2018-08-27 20:56:16.550 18208 [job-4] INFO com.qbian.redis.RedisLockTest - ----------> j=5
2018-08-27 20:56:16.550 18208 [job-4] INFO com.qbian.common.common.RedisLock - thread(job-4) release redis lock .
2018-08-27 20:56:16.551 18209 [job-4] INFO com.qbian.common.common.RedisLock - thread(job-4) release local lock .
2018-08-27 20:56:16.551 18209 [job-5] INFO com.qbian.common.common.RedisLock - thread(job-5) get the local lock successfully .
2018-08-27 20:56:16.552 18210 [job-5] INFO com.qbian.common.common.RedisLock - thread(job-5) get the redis lock successfully .
2018-08-27 20:56:16.553 18211 [job-5] INFO com.qbian.redis.RedisLockTest - ----------> j=6
2018-08-27 20:56:16.554 18212 [job-5] INFO com.qbian.common.common.RedisLock - thread(job-5) release redis lock .
2018-08-27 20:56:16.554 18212 [job-5] INFO com.qbian.common.common.RedisLock - thread(job-5) release local lock .
2018-08-27 20:56:16.554 18212 [job-6] INFO com.qbian.common.common.RedisLock - thread(job-6) get the local lock successfully .
2018-08-27 20:56:16.556 18214 [job-6] INFO com.qbian.common.common.RedisLock - thread(job-6) get the redis lock successfully .
2018-08-27 20:56:16.556 18214 [job-6] INFO com.qbian.redis.RedisLockTest - ----------> j=7
2018-08-27 20:56:16.557 18215 [job-6] INFO com.qbian.common.common.RedisLock - thread(job-6) release redis lock .
2018-08-27 20:56:16.557 18215 [job-6] INFO com.qbian.common.common.RedisLock - thread(job-6) release local lock .
2018-08-27 20:56:16.557 18215 [job-7] INFO com.qbian.common.common.RedisLock - thread(job-7) get the local lock successfully .
2018-08-27 20:56:16.559 18217 [job-7] INFO com.qbian.common.common.RedisLock - thread(job-7) get the redis lock successfully .
2018-08-27 20:56:16.559 18217 [job-7] INFO com.qbian.redis.RedisLockTest - ----------> j=8
2018-08-27 20:56:16.560 18218 [job-7] INFO com.qbian.common.common.RedisLock - thread(job-7) release redis lock .
2018-08-27 20:56:16.560 18218 [job-7] INFO com.qbian.common.common.RedisLock - thread(job-7) release local lock .
2018-08-27 20:56:16.560 18218 [job-8] INFO com.qbian.common.common.RedisLock - thread(job-8) get the local lock successfully .
2018-08-27 20:56:16.562 18220 [job-8] INFO com.qbian.common.common.RedisLock - thread(job-8) get the redis lock successfully .
2018-08-27 20:56:16.562 18220 [job-8] INFO com.qbian.redis.RedisLockTest - ----------> j=9
2018-08-27 20:56:16.563 18221 [job-8] INFO com.qbian.common.common.RedisLock - thread(job-8) release redis lock .
2018-08-27 20:56:16.563 18221 [job-8] INFO com.qbian.common.common.RedisLock - thread(job-8) release local lock .
2018-08-27 20:56:16.563 18221 [job-9] INFO com.qbian.common.common.RedisLock - thread(job-9) get the local lock successfully .
2018-08-27 20:56:16.565 18223 [job-9] INFO com.qbian.common.common.RedisLock - thread(job-9) get the redis lock successfully .
2018-08-27 20:56:16.566 18224 [job-9] INFO com.qbian.redis.RedisLockTest - ----------> j=10
2018-08-27 20:56:16.568 18226 [job-9] INFO com.qbian.common.common.RedisLock - thread(job-9) release redis lock .
2018-08-27 20:56:16.568 18226 [job-9] INFO com.qbian.common.common.RedisLock - thread(job-9) release local lock .