在单机开发过程中,对于线程并发问题我们可以通过加锁来限制执行
但是在分布式系统的开发过程中,单机锁对于不同机器实例不同jvm对同一业务或资源的操作却不能生效
因此我们需要使用分布式锁来解决分布式情况下的多进程并发问题
以下主要是记录基于Redis/Zookeeper实现的简单的自定义分布式锁
文章目录
- 基于Redis的分布式锁
- Redis优点
- 基于redis的分布式锁
- Redission实现的分布式锁
- 基于Zookeeper的分布式锁
- zookeeper作用
- 基于zookeeper的分布式锁
- 测试
基于Redis的分布式锁
Redis优点
redis是目前我们开发过程中经常用的缓存数据库,它的优点也十分明显:
- 性能极高:基于内存的单线程操作,使得它的读写性能极高。
- 数据类型丰富:除了简单的数据类型之外,还支持list,set,zset,hash等
- 原子性:单个操作和多个操作(事务)都支持原子性
- 持久化:可以将数据写入磁盘
- 可备份:主从模式
基于redis的分布式锁
/**
* 基于Redis的分布式锁
* 使用后手动清除或者等待超时失效
*/
@Slf4j
@Component
@ConditionalOnProperty(prefix = "custom.dslock.redis", name = "enable", havingValue = "true")
public class RedisDSLock {
protected static long INTERNAL_LOCK_LEASE_TIME = 3000;
//过期时长设置及规则 key不存在时插入
private static SetParams params = SetParams.setParams().nx().px(INTERNAL_LOCK_LEASE_TIME);
//jedis配置
static JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
static JedisPool jedisPool ;
//初始化Jedis
private static void initJedis(){
if(null == jedisPool){
//最大空闲数
int maxIdle = 50;
//最大连接数
int maxTotal = 100;
//最大等待时长
int maxWaitMilis = 3000;
String ip = "127.0.0.1";
int port = 6379;
//读取配置
try {
maxIdle =SpringContextUtils.containProperty("custom.dslock.redis.maxIdle",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxIdle"):maxIdle;
maxTotal = SpringContextUtils.containProperty("custom.dslock.redis.maxTotal",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxTotal"):maxTota;
maxWaitMilis = SpringContextUtils.containProperty("custom.dslock.redis.maxWaitMilis",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxWaitMilis"):maxWaitMilis;
if(SpringContextUtils.containProperty("custom.dslock.redis.ip",true)){
ip = SpringContextUtils.getProperty("custom.dslock.redis.ip").toString();
}else {
log.warn("cannot get the property [ custom.dslock.redis.ip ] for redis ip, using default ip [ 127.0.0.1 ] ");
}
if(SpringContextUtils.containProperty("custom.dslock.redis.port",true)){
port = (int) SpringContextUtils.getProperty("custom.dslock.redis.port");
}else{
log.warn("cannot get the property [ custom.dslock.redis.port ] for redis port, using default port [ 6379 ] ");
}
}catch (Exception ex){
log.error("initJedis error",ex.getMessage());
}
//设置最大空闲数
jedisPoolConfig.setMaxIdle(maxIdle);
//最大连接数
jedisPoolConfig.setMaxTotal(maxTotal);
//最大等待毫秒数
jedisPoolConfig.setMaxWaitMillis(maxWaitMilis);
jedisPool = new JedisPool(jedisPoolConfig,ip, port);
}
}
/**
* 申请锁
* @param key 主键
* @param value 值
* @return
*/
public static boolean lock(String key, String value) {
initJedis();
try( Jedis jedis = jedisPool.getResource()){
//三秒后过期
String lock = jedis.set(key, value,params);
if ("OK".equals(lock)) {
return true;
}else{
return false;
}
}
}
/**
* 申请并等待锁
* @param key 主键
* @param value 值
* @param waitTime 等待时长 毫秒
* @return
*/
public static boolean trylock(String key, String value,long waitTime) {
initJedis();
Long start = System.currentTimeMillis();
for (; ; ) {
if(System.currentTimeMillis()-start>waitTime){
log.error("wait for lock out of time");
return false;
}
if(tryLock(key, value))
return true;
}
}
/**
* 释放锁
* @param key 主键
* @param value 值
*/
public static boolean unlock(String key, String value) {
initJedis();
try(Jedis jedis = jedisPool.getResource()){
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
String result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)).toString();
return "1".equals(result) ? true : false;
} finally {
jedis.close();
}
}
}
}
Redission实现的分布式锁
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
初始化redissionClient(支持单机模式、哨兵模式、集群模式)
//单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setPassword("zdm371326")
.setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
//哨兵模式
//哨兵模式的分布式锁可能会出现脏数据:
//当程序1在master节点获得了锁,并向slave节点同步的时master节点宕机,slave节点成为新的master
//程序2向新的master节点申请锁,这样程序1和2都获得了锁
Config config = new Config();
config.useSentinelServers().addSentinelAddress(
"redis://127.0.0.1:6379","redis://127.0.0.1:6378", "redis://127.0.0.1:6377")
.setConnectTimeout(30000)//连接超时时长
.setReconnectionTimeout(10000)//连接断开后等待连接的时间间隔
.setTimeout(10000)//等待返回信息的超时时长
.setRetryAttempts(5)//发送命令的最大尝试次数
.setRetryInterval(3000)//每次重试的时间间隔
.setMasterName("redismaster")
.setPassword("zdm371326").setDatabase(0);
//集群模式
Config config = new Config();
config.useClusterServers().addNodeAddress(
"redis://127.0.0.1:6379","redis://127.0.0.1:6378", "redis://127.0.0.1:6377")
.setScanInterval(5000)//设置集群状态扫描时间
.setMasterConnectionPoolSize(10000)//设置Master节点最大连接数
.setSlaveConnectionPoolSize(10000)//设置Slave节点最大连接数
.setReconnectionTimeout(10000)//连接断开后等待连接的时间间隔
.setTimeout(10000)//等待返回信息的超时时长
.setRetryAttempts(5)//发送命令的最大尝试次数
.setRetryInterval(3000)//每次重试的时间间隔
.setPassword("zdm371326");
redission内部封装好了各种锁,而且均支持支持自动解锁。
- 可重入锁
RLock lock = redisson.getLock("lock");
//lock.lock();
//lock.lock(10, TimeUnit.SECONDS);//加锁10秒后自动解锁
//尝试加锁,等待100秒,10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
//lock.lockAsync();//异步加锁
//lock.lockAsync(10, TimeUnit.SECONDS);//异步加锁10秒后自动解锁
//Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
lock.unlock();
- 公平锁
RLock fairLock = redisson.getFairLock("lock");
- 联锁和红锁
//可以是不同redis实例的锁
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock2 = redisson.getLock("lock3");
//联锁
RedissonMultiLock multiLock= new RedissonMultiLock(lock1,lock2,lock3);
//当lock1 lock2 lock3都获取到时,加锁成功
multiLock.lock();
multiLock.unlock();
//红锁
RedissonRedLock redLock= new RedissonRedLock (lock1,lock2,lock3);
//当获取到大部分锁时,加锁成功
redLock.lock();
redLock.unlock();
- 读写锁
//读写锁 一写多读
RReadWriteLock rwLock = redisson.getReadWriteLock("lock");
rwLock.readLock().lock();
rwLock.writeLock().lock();
rwLock.unlock();
基于Zookeeper的分布式锁
zookeeper作用
ZooKeeper 是一个典型的分布式数据一致性解决方案,作为Hadoop项目下的一个子项目,其也是一个相当成熟优秀的应用。分布式应用程序可以基于 ZooKeeper 实现数据发布/订阅、负载均衡、分布式锁、分布式协调/通知、集群管理、Master 选举和分布式队列等功能。
作分布式锁的缺点:
ZK通过对节点的动态新增并判断当前线程所拥有的node编号是不是最小的编号来确定是否获取到锁,因此性能远不如Redis。ZK节点之间的网络异常可能会导致并发现象(可能性很小)。
作分布式锁的优点:
高可用,解决失效导致的死锁。避免某个线程获得锁之后应用宕掉而导致的死锁,客户端断开连接之后,ZK会删除相应节点,这样其他应用可以立即获取锁资源。
基于zookeeper的分布式锁
写一个函数式接口,@FunctionalInterface 注解只允许有一个抽象方法存在。
@FunctionalInterface
public interface ZKDSWorker {
void todo();
}
分布式锁实现类
@Slf4j
public class ZookeeperDSLock implements MediaDisposer.Disposable {
private CountDownLatch cdl = new CountDownLatch(1);
//IP PORT
private String IP_PORT ;
//父节点
private String PARENT_NODE ;
//连接超时
private int connectionTimeout;
//会话超时
private int sessionTimeout;
private ZkClient zkClient ;
//记录紧前节点
private volatile String beforePath;
//记录当前节点
private volatile String path;
//记录节点目录
private volatile List<String> children = new ArrayList<>();
public static class Builder {
//初始化节点和zk地址
private String ipPort= "127.0.0.1:2181";
private String pNode= "/LOCKNODE";
private int connectionTimeout = 3000;
private int sessionTimeout = 3000;
public Builder() {
}
public void setIpPort(String ipPort){
this.ipPort=ipPort;
}
public void setPNode(String pNode){
this.pNode=pNode;
}
public void setConnectionTimeout(int connectionTimeout){
this.connectionTimeout=connectionTimeout;
}
public void setSessionTimeout(int sessionTimeout){
this.sessionTimeout=sessionTimeout;
}
public ZookeeperDSLock build(){
return new ZookeeperDSLock(this);
}
}
private ZookeeperDSLock(@NotNull Builder builder) {
//初始化信息
this.IP_PORT=builder.ipPort;
this.PARENT_NODE=builder.pNode;
this.connectionTimeout = builder.connectionTimeout;
this.sessionTimeout = builder.sessionTimeout;
try {
zkClient = new ZkClient(IP_PORT, sessionTimeout, connectionTimeout);
}catch (Exception ex){
throw new RuntimeException("ZkClient 初始化失败:"+ex.getMessage());
}
//父节点不存在则先创建
if (!zkClient.exists(PARENT_NODE)) {
try {
zkClient.createPersistent(PARENT_NODE);
}catch (Exception ex){
throw new RuntimeException("ZkClient 父节点创建失败:");
}
}
}
//获得锁
public synchronized boolean lock(String lockObj) {
// 当前的是最小节点就返回加锁成功
if (getLock(lockObj)) {
return true;
} else {
//删掉当前节点 没有获取锁的话 删除临时节点
zkClient.deleteRecursive(path);
return false;
}
}
//排队等到锁 执行任务
public void doOnWaitLock(String lockObj,ZKDSWorker zkdsWorker) {
if(getLock(lockObj)){
// 获得锁
zkdsWorker.todo();
//释放锁
this.unlock(lockObj);
}else{
//未得到锁 对节点进行监听
waitForLock(lockObj,zkdsWorker);
}
}
//释放锁
public void unlock(String lockObj) {
if (Strings.isBlank(path)) {
path = zkClient.createEphemeralSequential(PARENT_NODE + "/", lockObj);
}
//遍历子节点删除所有子节点后再删除目标目录
zkClient.deleteRecursive(path);
}
//资源释放
@Override
public void dispose() {
zkClient.close();
}
//判断是不是可以获得到锁
private boolean getLock(String lockObj){
// 创建自己的临时节点
if (Strings.isBlank(path)) {
path = zkClient.createEphemeralSequential(PARENT_NODE + "/", lockObj);
}
//最小节点可以获得锁
return isMinNode();
}
//判断当前任务创建的节点是不是最小节点
private boolean isMinNode(){
// 对节点排序
children = zkClient.getChildren(PARENT_NODE);
Collections.sort(children);
return path.equals(PARENT_NODE + "/" + children.get(0));
}
//等待锁
private void waitForLock(String lockObj,ZKDSWorker zkdsWorker) {
// 不是最小节点 就找到自己的前一个 依次类推 释放也是一样
int i = Collections.binarySearch(children, path.substring(PARENT_NODE.length() + 1));
beforePath = PARENT_NODE + "/" + children.get(i - 1);
IZkDataListener listener = new IZkDataListener() {
public void handleDataChange(String s, Object o) throws Exception {
}
public void handleDataDeleted(String s) throws Exception {
cdl.countDown();
}
};
// 监听 监听到变化之后 需要对节点重新建立监听
this.zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(beforePath)) {
try {
//等待加锁
cdl.await();
//再次判断是不是最小节点
if(isMinNode()){
zkdsWorker.todo();
// 最后释放监听
zkClient.unsubscribeDataChanges(beforePath, listener);
this.unlock(lockObj);
}else{
// 释放监听
zkClient.unsubscribeDataChanges(beforePath, listener);
//对节点重新进行监听
waitForLock(lockObj,zkdsWorker);
}
} catch (InterruptedException e) {
log.error("加锁失败",e);
}
}
}
}
测试
创建多个线程同时加锁
IntStream.rangeClosed(1,10).parallel().boxed().forEach(
e->{
try {
testService.zkLock(e);
}catch (Exception ex){
log.error(ex.getMessage());
}
});
testService.zkLock()实现内容
//1、加锁
ZookeeperDSLock zookeeperDSLock= new ZookeeperDSLock.Builder().build();
if( zookeeperDSLock.lock("testLock")){
System.out.println(inr+" locked" );
try {
TimeUnit.MILLISECONDS.sleep(3000);
}catch (Exception ex){
log.error(ex.getMessage());
}
zookeeperDSLock.unlock("testLock");
}else{
System.out.println(inr+" lock failed" );
}
//2、多线程获取锁失败时 等待锁
SimpleDateFormat sf=new SimpleDateFormat("yyyy-dd-MM HH:mm:ss:SSS");
try {
zookeeperDSLock.doOnWaitLock("locktest",()->{
String time =sf.format(new Date());
System.out.println(inr+" "+time+" i am working");
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}catch (Exception ex){
ex.printStackTrace();
}