背景
由于分布式或者集群部署项目时,在某些业务场景下需保证资源的原子性、一致性和互斥性。
如果把房子比作资源,通俗的来讲,我无论在那个城市生活,这个房子我先租的,再没有退房的前提下,别人都不能用
解决方案
目前最流行的解决方案
- redisson 分布式锁
- zookeeper 分布式锁
redisson 分布式锁 实战
- maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
- 配置文件
server:
port: 8081
spring:
redis:
host: 192.168.10.10
port: 6379
- java代码
package com.gz.distributed.lock.service.impl;
import com.gz.distributed.lock.service.LockService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public class LockServiceImpl implements LockService {
private final RedissonClient redisson;
@Override
public void testDistributedLock() throws InterruptedException {
// 获取锁实例
RLock lock = redisson.getLock("myLock");
// 加锁 第一个参数 100 代表获取锁的时间 第二参数 10 代表 锁的时间,自动释放
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
log.info("hello redisson");
} finally {
lock.unlock();
}
}
}
}
- 原理
在探索原理之前,我想提出以下问题
1.是如何保证原子性,互斥性
2.如果锁超时了怎么办
图示:
注明:该图片来源于网络,如涉及侵权,请告知
第一步 :根据锁名字 创建锁示例
RLock lock = redisson.getLock("myLock");
第二步:尝试加锁(核心逻辑)
boolean res = lock.tryLock(100, 10, TimeUnit.MINUTES);
、 public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
//尝试加锁,返回锁的过期时间
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
// ttl为空, 说明加锁成功,返回true
if (ttl == null) {
return true;
}
//判断获取锁是否超时
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
// 订阅监听redis消息,并且创建RedissonLockEntry
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
// 阻塞等待subscribe的future的结果对象
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + time + "ms. " +
"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException e) {
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
//判断是否超时,如果等待超时,返回获的锁失败
、 time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
//通过while循环再次尝试竞争锁
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
// 通过信号量(共享锁)阻塞,等待解锁消息. (减少申请锁调用的频率)
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
第三步骤:lua 脚本保持原子性,互斥性
return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
keys[1] : Collections.singletonList(getRawName()) 就是锁的名称
ARGV[1]: leaseTime 租约时间
ARGV[2]:getLockName(threadId) = UUID.randomUUID().toString()+线程id
lua脚本:
判断该锁是否存在,存在则返回锁的租约时间,不存在设置过期时间,锁加1(就是锁重入)
注明:该图片来源于网络,如涉及侵权,请告知
缺点:
客户端1 对某个master节点写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生
master节点宕机,主备切换,slave节点从变为了 master节点。这时客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
缺陷在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。
zookeeper 分布式锁 实战
- maven 依赖
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zookeeper</artifactId>
<version>5.5.18</version>
</dependency>
- config
/**
* zookeeper lock config
*/
@Configuration
public class ZookeeperLockConfiguration {
/**
* 注册表
* @param curatorFramework
* @return
*/
@Bean
public ZookeeperLockRegistry zookeeperLockRegistry(CuratorFramework curatorFramework) {
return new ZookeeperLockRegistry(curatorFramework, "/locks");
}
/**
* 客户端
* @return
* @throws Exception
*/
@Bean
public CuratorFramework curatorFramework() throws Exception {
return CuratorFrameworkFactory.newClient("127.0.0.1:2181", new RetryUntilElapsed(1000, 4));
}
}
- java代码
public void testZookeeperLock() {
Lock lock = zookeeperLockRegistry.obtain("my-lock");
if (lock.tryLock()) {
try {
log.info("hello testZookeeperLock");
} finally {
lock.unlock();
}
}
}
- 原理
利用 Zookeeper 节点的临时性:当一个进程崩溃或断开连接时,它创建的节点会被自动删除
利用 Zookeeper 节点的顺序性:Zookeeper 中的节点有序排列,每个节点都有一个唯一的编号。进程获取锁时,会创建一个带有序号的节点,然后判断自己是否是最小的节点。如果是最小的节点,则获取锁成功;否则,进程需要等待
结论
zookeeper 是强一致性,不保证服务可用性,主从切换时,服务都不可用
redisson 不保证数据一致性,会出现访问同一资源不安全性发生
根据不同业务应用场景,项目架构选择不同技术方案实现
代码地址
https://gitee.com/GZ-jelly/microservice-sample