1、概述
在并发编程中,锁是用来保证数据同步的重要举措,Java 自带了多种锁的实现,如synchronized、ReentrantLock、ReentrantReadWriteLock等。但这些锁只在该进程内有效。一但跨进程就会不起作用。然而在当下的技术趋势下,微服务,单服务多实例,多实例间负载均衡已经成为常见的架构基础。在这样的多进程情况下,要保证同步,那么分布式锁就尤为重要。
这篇文章,描述了两种Redis分布式锁的实现方式。希望对大家有用。
2、原理
锁的原理来自于其“唯一持有性”,因此只要能保证这个唯一持有性,那么很多东西都能作为锁。例如数据库、文件、Redis等。
其原理大概如下:
A 尝试建立资源
在A之前没有这个资源
A建立资源成功,A获得了锁
B尝试建立资源
在B之前A已经建立了资源
B建立资源失败,B未获得锁
A过了一段时间删除了该资源,释放了锁
B尝试建立资源
在B之前没有这个资源
B建立资源成功,B获得了锁
将这个资源套入到Mysql的某一行、某一个文件、Redis的某一个Key都可以成立。
但这个成立,需要一个条件,那就是这个建立锁的过程必须是原子性的。
也就是说
A 尝试建立资源
在A之前没有这个资源
A建立资源成功,A获得了锁
这个过程必须完全执行,不能中断。
Redis是单线程的。因此只要保证一条命令执行完这个建立锁的过程,就可以保证这个锁是有效的。
我们刚好有两种法子。
1、set k v nx px ms
原子操作 版 set 命令(nx 表示仅当这个key不存在时才执行set,px 设置过期时间,单位是毫秒)
2、通过lua脚本来执行这个建立锁的过程(redis 执行lua脚本也是原子性的)
分别对应着下文的两种锁的实现。
3、set k v nx px ms
版
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 用redis 实现的分布式锁
*
* set k v nx px ms 原子操作 版
*/
@Slf4j
public class RedisLock {
private final static String LOCK_KEY_PRE = "lock_";
// 此处需要设置可见性,以保证同步
private volatile boolean locked = false;
private final RedisTemplate<String, Object> redisTemplate;
private final String lockMaster;
private final String lockKey;
private String lockValue;
private final long expireMillis;
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockMaster,
String lockKey, long expireMillis) {
Assert.isTrue(StringUtils.hasLength(lockMaster), "lockMaster不能为空");
Assert.isTrue(StringUtils.hasLength(lockKey), "lockKey不能为空");
Assert.isTrue(expireMillis > 0, "expireMillis 需要大于 0");
this.redisTemplate = redisTemplate;
this.lockMaster = lockMaster;
this.lockKey = LOCK_KEY_PRE + lockKey;
this.expireMillis = expireMillis;
}
/**
* 尝试获取锁,获取不到立即返回
*
* @return 是否获得锁
*/
public boolean getLock() {
// 通过UUID生成一个随机的字符串,作为锁的值,解锁时除了需要对比这个值是否一致,类似于一把钥匙
lockValue = UUID.randomUUID().toString();
locked = this.setLock(lockKey, lockValue, expireMillis);
log.info("{} 尝试获得锁{},结果{}", lockMaster, lockKey, locked);
return locked;
}
/**
* 尝试获取锁,等待一段时间
*
* @param timeout 毫秒
* @return 是否获得锁
*/
public boolean tryGetLock(long timeout) {
// 生成随机value
lockValue = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) <= timeout) {
if (this.setLock(lockKey, lockValue, expireMillis)) {
locked = true;
log.info("{} 获得到锁{},预计阻塞时间{},实际阻塞时间:{}ms",
lockMaster, lockKey, timeout, System.currentTimeMillis() - startTime);
return true;
}
sleep();
}
log.info("{} 获得锁{}失败!预计阻塞时间{},实际阻塞时间:{}ms",
lockMaster, lockKey, timeout, System.currentTimeMillis() - startTime);
locked = false;
return false;
}
/**
* 阻塞获取锁,一直阻塞直到获取锁
*
* @return true
*/
public boolean tryGetLockBlock() {
// 生成随机value
lockValue = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
while (true) {
if (this.setLock(lockKey, lockValue, expireMillis)) {
locked = true;
log.info("{} 获得到锁{},阻塞时间:{}ms",
lockMaster, lockKey, System.currentTimeMillis() - startTime);
return true;
}
sleep();
}
}
/**
* 解锁
*
* @return 解锁是否成功
*/
public boolean unlock() {
Boolean unlockOk = false;
// 持有锁才能释放锁
if (locked) {
if (checkLock(lockKey, lockValue)) {
unlockOk = redisTemplate.execute((RedisConnection conn) -> {
Long count = conn.keyCommands().del(lockKey.getBytes());
return count != null && count > 0;
});
unlockOk = unlockOk != null && unlockOk;
locked = !unlockOk;
log.info("{} 释放锁{}结果:{}", lockMaster, lockKey, unlockOk);
}
}
return unlockOk;
}
private void sleep() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
log.warn("RedisLock " + lockKey + " 被外部中断");
}
}
private Boolean checkLock(final String key, final String value) {
return value.equals(redisTemplate.execute((RedisConnection conn) -> {
byte[] bytes = conn.stringCommands().get(key.getBytes());
if (bytes != null) {
return new String(bytes);
}
return null;
}));
}
Boolean setLock(String key, String value, long milliSeconds) {
// set k v nx px ms 原子操作
return redisTemplate.execute((RedisConnection conn) -> {
Boolean result = conn.stringCommands().set(
key.getBytes(),
value.getBytes(),
Expiration.from(milliSeconds, TimeUnit.MILLISECONDS),
RedisStringCommands.SetOption.SET_IF_ABSENT);
log.info("{} 尝试获取锁{}:{} ", lockMaster, lockKey, result);
return result != null && result;
});
}
public boolean isLocked() {
return locked;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public String getLockMaster() {
return lockMaster;
}
public String getLockKey() {
return lockKey;
}
public String getLockValue() {
return lockValue;
}
public long getExpireMillis() {
return expireMillis;
}
}
代码并不复杂,提供的获取锁的方法有三个:
1、boolean getLock() 尝试获取锁,获取不到立即返回
2、boolean tryGetLock(long timeout) 尝试获取锁,等待一段时间
3、boolean tryGetLockBlock() 阻塞获取锁,一直阻塞直到获取锁
释放锁的方法:
boolean unlock()
判断是否持有锁的方法:
boolean isLocked()
4、使用 lua 脚本实现的原子操作版
为了保持代码简洁,此处直接继承了RedisLock 实现代码复用。通过覆盖了两个方法,来设置不同的处理过程。
对于Redis使用lua脚本,可以看我的这一篇文章,里边讲的很详细:Redis 使用lua脚本。
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Arrays;
/**
* 用redis 实现的分布式锁
*
* 使用 lua 脚本实现的原子操作版
* 继承了RedisLock从而复用了一些方法
*/
@Slf4j
public class RedisLockUseLua extends RedisLock {
// 加锁脚本,成功返回1,失败返回0
private final static String LOCK_LUA_SCRIPT =
"if redis.call('get',KEYS[1]) then " +
"return 0;" +
"else " +
"redis.call('set',KEYS[1],ARGV[1]);" +
"redis.call('pexpire',KEYS[1],ARGV[2]);" +
"return 1;" +
"end;";
// 解锁脚本,成功返回1,失败返回0
private final static String UNLOCK_LUA_SCRIPT =
"local v = redis.call('get',KEYS[1]);" +
"if v then " +
"if v~=ARGV[1] then " +
"return 0;" +
"end;" +
"redis.call('del',KEYS[1]);" +
"end;" +
"return 1;";
public RedisLockUseLua(RedisTemplate<String, Object> redisTemplate, String lockMaster, String lockKey, long expireMillis) {
super(redisTemplate, lockMaster, lockKey, expireMillis);
}
@Override
public boolean unlock() {
boolean isOk = false;
// 持有锁才能释放锁
if (isLocked()) {
isOk = evalLuaScriptReturnBool(UNLOCK_LUA_SCRIPT,
new String[]{ getLockKey() }, getLockValue());
log.info("{} 释放锁{}结果:{}", getLockMaster(), getLockKey(), isOk);
}
return isOk;
}
@Override
Boolean setLock(String key, String value, long milliSeconds) {
return evalLuaScriptReturnBool(LOCK_LUA_SCRIPT,
new String[]{ key },
value, String.valueOf(milliSeconds));
}
private Boolean evalLuaScriptReturnBool(String luaScript, String[] keys, String... argv) {
return getRedisTemplate().execute(RedisScript.of(luaScript, Boolean.class),
new StringRedisSerializer(),
new GenericToStringSerializer<>(Boolean.class),
Arrays.asList(keys),
(Object[]) argv);
}
}
5、测试和验证
通过Junit 写了几个测试用例,验证了一下。同时也大概演示了如何使用:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DawnRedisLockTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
private void sleep(long milli) {
try {
Thread.sleep(milli);
} catch (InterruptedException ignore) {
}
}
@Test
public void testRedisLock1() throws InterruptedException {
testLock1(RedisLock::new);
}
@Test
public void testRedisLockUseLua1() throws InterruptedException {
testLock1(RedisLockUseLua::new);
}
@Test
public void testRedisLock2() throws InterruptedException {
testLock2(RedisLock::new);
}
@Test
public void testRedisLockUseLua2() throws InterruptedException {
testLock2(RedisLockUseLua::new);
}
@FunctionalInterface
interface CreateRedisLock {
RedisLock create(RedisTemplate<String, Object> redisTemplate, String lockMaster, String lockKey, long expireMillis);
}
// 模拟高并发下,多线程竞争瞬间是否只有一个线程可以获得到锁,测试50次,每次产生40条线程竞争锁
public void testLock1(CreateRedisLock createRedisLock) throws InterruptedException {
final int testTimes = 50;
for (int i = 0; i < testTimes; i++) {
final AtomicInteger atomicInteger = new AtomicInteger(1);
final AtomicInteger lockNum = new AtomicInteger(0);
Supplier<Runnable> supplier = () -> () -> {
int j = atomicInteger.getAndIncrement();
RedisLock redisLock = createRedisLock.create(redisTemplate, "runnable" + j, "TestLock", 100);
System.out.printf("runnable%d 执行\n", j);
if (redisLock.getLock()) {
System.out.printf("runnable%d 获得锁\n", j);
lockNum.incrementAndGet();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
}
};
final int threadNum = 40;
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
for (int j = 0; j < threadNum; j++) {
executorService.submit(supplier.get());
}
executorService.shutdown();
//noinspection ResultOfMethodCallIgnored
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
Assert.assertEquals(1, lockNum.get());
}
}
// 模拟锁的获得过程来校验其是否是按照预期的
public void testLock2(CreateRedisLock createRedisLock) throws InterruptedException {
String result = "runnable1 获得锁runnable2 未获得锁runnable3 获得锁runnable4 获得锁";
StringBuilder sb = new StringBuilder();
Runnable runnable1 = () -> {
RedisLock redisLock = createRedisLock.create(redisTemplate, "runnable1", "TestLock", 100);
System.out.println("runnable1 执行");
if (redisLock.getLock()) {
System.out.println("runnable1 获得锁");
sb.append("runnable1 获得锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
}
};
Runnable runnable2 = () -> {
RedisLock redisLock = new RedisLock(redisTemplate, "runnable2", "TestLock", 100);
sleep(30);
System.out.println("runnable2 执行");
if (redisLock.getLock()) {
System.out.println("runnable2 获得锁");
sb.append("runnable2 获得锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
} else {
System.out.println("runnable2 未获得锁");
sb.append("runnable2 未获得锁");
}
};
Runnable runnable3 = () -> {
RedisLock redisLock = new RedisLock(redisTemplate, "runnable3", "TestLock", 100);
sleep(60);
System.out.println("runnable3 执行");
if (redisLock.tryGetLock(101)) {
System.out.println("runnable3 获得锁");
sb.append("runnable3 获得锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
}
};
Runnable runnable4 = () -> {
RedisLock redisLock = new RedisLock(redisTemplate, "runnable4", "TestLock", 100);
sleep(150);
System.out.println("runnable4 执行");
if (redisLock.tryGetLockBlock()) {
sb.append("runnable4 获得锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(runnable1);
executorService.submit(runnable2);
executorService.submit(runnable3);
executorService.submit(runnable4);
executorService.shutdown();
//noinspection ResultOfMethodCallIgnored
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
Assert.assertEquals(result, sb.toString());
}
}
使用这个锁时要保证在异常情况下锁能得到释放,因此要使用try catch finally的格式,在finally里释放锁。
像这样:
if (redisLock.tryGetLockBlock()) {
try {
// 实际业务代码
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
redisLock.unlock();
}
}
测试的结果:
测试结果的分析:
两种Redis分布式锁的实现都能保证在同一刻只有一个线程持有锁。
使用set k v nx px ms 的实现和lua的脚本的实现从耗时来说基本一致,lua脚本的会稍快一丢丢。
6、多进程下模拟锁竞争
多进程下锁竞争的模拟是通过定时任务实现的,对于同一个实例,分别启动了五个实例,五个实例的端口不一,但是定时任务的时间一致。
@Component
public class RedisLockScheduled {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(cron = "0 * * * * ?")
public void getRedisLock() {
RedisLock redisLock = new RedisLock(redisTemplate,
"RedisLockScheduled", "RedisLock", 2333);
if (redisLock.getLock()) {
System.out.println("\n\n\n\n获得锁!!!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redisLock.unlock();
}
}
}
@Scheduled(cron = "0 * * * * ?")
public void getRedisLockUseLua() {
RedisLock redisLock = new RedisLockUseLua(redisTemplate,
"RedisLockScheduled", "RedisLockUseLua", 2333);
if (redisLock.getLock()) {
System.out.println("\n\n\n\n获得锁!!!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redisLock.unlock();
}
}
}
}
以下是五个进程的日志:
2021-06-16 00:00:00.001 进程1获得了锁lock_RedisLockUseLua
2021-06-16 00:00:00.024 进程4获得了锁lock_RedisLock
2021-06-16 00:00:02.002 进程4释放锁lock_RedisLock
2021-06-16 00:00:02.025 进程4释放锁lock_RedisLockUseLua
2021-06-16 00:00:02.026 进程1获得了锁lock_RedisLock
1:
111111111111111111111111111111111
2021-06-16 00:00:00.001 INFO 41580 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果true
获得锁RedisLockUseLua!!!
2021-06-16 00:00:02.002 INFO 41580 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 释放锁lock_RedisLockUseLua结果:true
111111111111111111111111111111111
2021-06-16 00:00:02.026 INFO 41580 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获取锁lock_RedisLock:true
2021-06-16 00:00:02.026 INFO 41580 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLock,结果true
获得锁RedisLock!!!
2021-06-16 00:00:04.030 INFO 41580 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 释放锁lock_RedisLock结果:true
2:
111111111111111111111111111111111
2021-06-16 00:00:00.014 INFO 43260 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false
失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027 INFO 43260 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获取锁lock_RedisLock:false
2021-06-16 00:00:00.027 INFO 43260 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false
失败:未获得锁RedisLock!!!
3:
111111111111111111111111111111111
2021-06-16 00:00:00.001 INFO 38504 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false
失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027 INFO 38504 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获取锁lock_RedisLock:false
2021-06-16 00:00:00.027 INFO 38504 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false
失败:未获得锁RedisLock!!!
4:
111111111111111111111111111111111
2021-06-16 00:00:00.001 INFO 41200 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false
失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.024 INFO 41200 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获取锁lock_RedisLock:true
2021-06-16 00:00:00.024 INFO 41200 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLock,结果true
获得锁RedisLock!!!
2021-06-16 00:00:02.025 INFO 41200 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 释放锁lock_RedisLock结果:true
5:
111111111111111111111111111111111
2021-06-16 00:00:00.001 INFO 14768 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false
失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027 INFO 14768 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获取锁lock_RedisLock:false
2021-06-16 00:00:00.027 INFO 14768 --- [ scheduling-1] cn.hengyumo.dawn.utils.RedisLock : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false
失败:未获得锁RedisLock!!!
END
Tell me why are we, so blind to see
That the one's we hurt, are you and me
版权声明
本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途
评论区#
还没有评论哦,期待您的评论!
引用发言