在单机应用中,当多个线程访问共享资源时,我们通常通过synchronized
关键字、Lock锁
、线程安全对象等措施保证资源的安全使用。
在分布式环境下,上述措施不再能满足需求,这事,我们需要一种应用于分布式换件的加锁机制,即:分布式锁。
分布式锁的实现方式有多重,如:数据库、Redis、ZooKeeper等等。
本文主要讲解Redis的分布式锁实现方式,主要依据官方文档:Distributed locks with Redis
一个分布式锁必须要满足的这个特性:
其实根据现状,基于Redis实现分布式锁最常见的方案是通过setnx命令:
# 当且仅当可以不存在时,将set (key,value),并返回1;否则返回0.
SET key random_value NX PX 30000
setnx通过以下措施来实现一个分布式锁:
独享
特性:
无死锁
特性:这个Key有过期时间,使它会最终释放。容错
:为了避免单点失败问题,构建master-slave架构,当master节点挂掉时,通过failover策略,将slave节点升级为master。为什么这里强调redis的单实例
,因为只有单实例
才能保证setnx的原子性。
通过之前的文章Redis: 单线程模型、I/O多路复用、影响性能的因素、性能与QPS,可以确定,即使是多个连接同时执行setnx,最终这些setnx命令依然是按顺序执行的,不存在并发的可能。
设置过期时间,使得即使因为其他原因(如:客户端崩溃、网络异常),这个键也会最终被释放掉,不会造成锁一直被占用的情况。
将value设置成随机值,是为了更安全的释放锁,避免误删别的客户端获取的锁。
我们先看看不设置随机值的情况:
可以看到,最终客户端A删掉了客户端B设置的可以。
下面,看一看设置了随机值的情况:
可以看到,每个客户端设置的key,分别持有唯一的随机值,可以防止自己的可以被其他客户端误删掉。
上述方案的问题在于:主从同步通常是异步的,并不能真正的容错。
造成锁不独享的场景如下图所示:
独享
特性相互违背。Redis官方提出一种算法,叫Redlock,认为这种实现比普通的单实例实现更安全。
RedLock有多种语言的实现包,其中Java版本的实现包叫做:Redisson
。
下面,描述如何通过Redisson实现分布式锁,并加以验证。
普通版本
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.11.0version>
dependency>
Spring Boot Starter
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.11.0version>
dependency>
/**
* RedLock的基本操作
*
* @author hanchao
*/
@Slf4j
public class RedLockDemo {
public static void main(String[] args) {
//连接redis
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
log.info("连接Redis");
//1.定义锁
RLock lock = redisson.getLock("myTest001");
try {
//尝试加锁的超时时间
Long timeout = 300L;
//锁过期时间
Long expire = 30L;
//2.获取锁
if (lock.tryLock(timeout, expire, TimeUnit.MILLISECONDS)) {
//2.1.获取锁成功的处理
log.info("加锁成功");
//...do something
log.info("使用完毕");
} else {
//2.2.获取锁失败的处理
log.info("加锁失败");
log.info("其他处理");
}
} catch (InterruptedException e) {
log.error("尝试获取分布式锁失败", e);
} finally {
//3.释放锁
try {
lock.unlock();
log.info("锁释放成功");
} catch (Exception e) {
//do nothing...
}
}
//关闭连接
redisson.shutdown();
log.info("关闭redis连接");
}
}
执行结果:
INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:25 - 连接Redis
INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:37 - 加锁成功
INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:39 - 使用完毕
INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:46 - 锁释放成功
INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:51 - 关闭redis连接
测试说明:1000个线程在线程池中执行,分别记录加锁成功的和失败的个数。
package pers.hanchao.basiccodeguideline.redlock;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
/**
* RedLock的简单并发测试
*
* @author hanchao
*/
@Slf4j
public class RedLockTestDemo {
public static void main(String[] args) throws InterruptedException {
//连接redis
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
//锁的名字
String key = "myTest001";
//尝试加锁的超时时间
Long timeout = 1000L;
//锁过期时间
Long expire = 30L;
//并发数
Integer size = 1000;
//定义线程池
ExecutorService executorService = Executors.newFixedThreadPool(size);
//定义倒计时门闩:以保证所有线程执行完毕再进行最后的计数
CountDownLatch latchCount = new CountDownLatch(size);
//计数器
LongAdder adderSuccess = new LongAdder();
LongAdder adderFail = new LongAdder();
//多线程执行
for (int i = 0; i < size; i++) {
executorService.execute(() -> {
//定义锁
RLock lock = redisson.getLock(key);
try {
//获取锁
if (lock.tryLock(timeout, expire, TimeUnit.MILLISECONDS)) {
//成功计数器累加1
adderSuccess.increment();
latchCount.countDown();
} else {
//失败计数器累加1
adderFail.increment();
latchCount.countDown();
}
} catch (InterruptedException e) {
log.error("尝试获取分布式锁失败", e);
} finally {
//释放锁
try {
lock.unlock();
} catch (Exception e) {
//do nothing
}
}
});
}
//等待所有线程执行完毕
latchCount.await();
//关闭线程池
executorService.shutdown();
//关闭连接
redisson.shutdown();
log.info("共计「{}」获取锁成功,「{}」获取锁失败。", adderSuccess.intValue(), adderFail.intValue());
}
}
测试结果:
INFO traceId: pers.hanchao.basiccodeguideline.redlock.RedLockTestDemo:84 - 共计「84」获取锁成功,「916」获取锁失败。