分布式锁多种实现方式
背景
单体环境下JavaAPI 提供了很多控制并发的接口,包括 synchronized 以及 JUC 下面的一些实现,随着互联网的发展,现在大部分的实力都是部署在不同的机器上,JavaAPI 提供的接口不能满足我们的需求,分布式锁就诞生。
分布式锁的概念
分布式锁就是在分布式环境下用来解决多实例对数据访问一致性的一种技术方案。
- 在分布式环境下同一时刻只能被单个线程获取(互斥性);
- 可重入,意思是已经获得锁的线程在执行的过程中不需要再次获得锁;
- 异常或者超时自动删除,避免死锁;
- 高性能,分布式环境下必须要性能好;
- 锁自动延期机制
使用场景
在实际环境中我们有很多场景会用到分布式锁,例如全局计数器,只要涉及到多个实例进程对同一份数据进行修改等操作都会需要分布式锁。在比如在下单,更新缓存,减少库存等场景下也会用到分布式锁的。
具体的实现
redisson 实现分布式锁
配置集群的用法
Config config = new Config();
ClusterServersConfig clusterServersConfig = config.useClusterServers();
clusterServersConfig.setFailedAttempts(3);
clusterServersConfig.addNodeAddress("redis://192.168.184.128:30001", "redis://192.168.184.128:30001");
clusterServersConfig.setScanInterval(1000);
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("key");
lock.lock();
lock.unlock();
clusterServersConfig配置参数解释
nodeAddresses: #配置集群地址
- “redis://127.0.0.1:6379”
- “redis://127.0.0.1:6380”
- “redis://127.0.0.1:6381”
- “redis://127.0.0.1:6382”
- “redis://127.0.0.1:6383”
- “redis://127.0.0.1:6384”
lockWatchdogTimeout: 30000 # 分布式锁自动过期时间,防止死锁,默认30000
- idleConnectionTimeout:10000 #连接空闲超时,单位:毫秒 默认10000
- pingTimeout: 1000
- connectTimeout: 10000 # 同任何节点建立连接时的等待超时。时间单位是毫秒 默认10000
- timeout: 3000 # 等待节点回复命令的时间。该时间从命令发送成功时开始计时。默认3000
- retryAttempts: 3 # 命令失败重试次数
- retryInterval: 1500 # 命令重试发送时间间隔,单位:毫秒
- reconnectionTimeout: 3000 # 重新连接时间间隔,单位:毫秒
- failedAttempts: 3 # 执行失败最大次数
- password: test1234 # 密码
- subscriptionsPerConnection: 5 # 单个连接最大订阅数量
- loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {} # loadBalancer 负载均衡算法类的选择
- slaveSubscriptionConnectionMinimumIdleSize: 1 #从节点发布和订阅连接的最小空闲连接数
- slaveSubscriptionConnectionPoolSize: 50 #从节点发布和订阅连接池大小 默认值50
- slaveConnectionMinimumIdleSize: 32 # 从节点最小空闲连接数 默认值32
- slaveConnectionPoolSize: 64 # 从节点连接池大小 默认64
- masterConnectionMinimumIdleSize: 32 # 主节点最小空闲连接数 默认32
- masterConnectionPoolSize: 64 # 主节点连接池大小 默认64
- subscriptionMode: SLAVE # 订阅操作的负载均衡模式
- readMode: SLAVE # 只在从服务器读取
- scanInterval: 1000 # 对Redis集群节点状态扫描的时间间隔。单位是毫秒。默认1000
- threads: 2 #这个线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享。默认2
- nettyThreads: 2 #这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,以及底层客户端所一同共享的线程池里保存的线程数量。默认2
codec: !<org.redisson.codec.JsonJacksonCodec> {} # 编码方式 默认org.redisson.codec.JsonJacksonCodec
transportMode: NIO #传输模式
redis set 实现分布式锁
set 命令解释
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
例如:
SET key-with-expire-and-NX “hello” EX 10 NX 当key不存在时,存入数据,过期时间为10秒
代码实现
package com.xkcoding.cache.redis.locl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* redis实现分布式锁
*
*/
public class RedisLockUtils {
private static final Logger log = LoggerFactory.getLogger(RedisLockUtils.class);
/**
* 默认轮休获取锁间隔时间, 单位:毫秒
*/
private static final int DEFAULT_ACQUIRE_RESOLUTION_MILLIS = 100;
private static final String UNLOCK_LUA;
static {
StringBuilder lua = new StringBuilder();
lua.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
lua.append("then ");
lua.append(" return redis.call(\"del\",KEYS[1]) ");
lua.append("else ");
lua.append(" return 0 ");
lua.append("end ");
UNLOCK_LUA = lua.toString();
}
private RedisTemplate redisTemplate;
private final ThreadLocal<Map<String, LockVO>> lockMap = new ThreadLocal<>();
public RedisLockUtils(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取锁,没有获取到则一直等待
*
* @param key redis key
* @param expire 锁过期时间, 单位 秒
*/
public void lock(final String key, long expire) {
try {
acquireLock(key, expire, -1);
} catch (Exception e) {
throw new RuntimeException("acquire lock exception", e);
}
}
/**
* 获取锁,指定时间内没有获取到,返回false。否则 返回true
*
* @param key redis key
* @param expire 锁过期时间, 单位 秒
* @param waitTime 获取锁超时时间, -1代表永不超时, 单位 秒
*/
public boolean tryLock(final String key, long expire, long waitTime) {
try {
return acquireLock(key, expire, waitTime);
} catch (Exception e) {
throw new RuntimeException("acquire lock exception", e);
}
}
/**
* 释放锁
*
* @param key redis key
*/
public void unlock(String key) {
try {
release(key);
} catch (Exception e) {
throw new RuntimeException("release lock exception", e);
}
}
/**
* @param key redis key
* @param expire 锁过期时间, 单位 秒
* @param waitTime 获取锁超时时间, -1代表永不超时, 单位 秒
* @return if true success else fail
* @throws InterruptedException 阻塞方法收到中断请求
*/
private boolean acquireLock(String key, long expire, long waitTime) throws InterruptedException {
//如果之前获取到了并且没有超时,则返回获取成功
boolean acquired = acquired(key);
if (acquired) {
return true;
}
long acquireTime = waitTime == -1 ? -1 : waitTime * 1000 + System.currentTimeMillis();
//同一个进程,对于同一个key锁,只允许先到的去尝试获取。
// key.intern() 如果常量池中存在当前字符串, 就会直接返回当前字符串.
// 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回
synchronized (key.intern()) {
String lockId = UUID.randomUUID().toString();
do {
long before = System.currentTimeMillis();
boolean hasLock = tryLock(key, expire, lockId);
//获取锁成功
if (hasLock) {
long after = System.currentTimeMillis();
Map<String, LockVO> map = lockMap.get();
if (map == null) {
map = new HashMap<>(2);
lockMap.set(map);
}
map.put(key, new LockVO(1, lockId, expire * 1000 + before, expire * 1000 + after));
log.debug("acquire lock {} {} ", key, 1);
return true;
}
Thread.sleep(DEFAULT_ACQUIRE_RESOLUTION_MILLIS);
} while (acquireTime == -1 || acquireTime > System.currentTimeMillis());
}
log.debug("acquire lock {} fail,because timeout ", key);
return false;
}
private boolean acquired(String key) {
Map<String, LockVO> map = lockMap.get();
if (map == null || map.size() == 0 || !map.containsKey(key)) {
return false;
}
LockVO vo = map.get(key);
if (vo.beforeExpireTime < System.currentTimeMillis()) {
log.debug("lock {} maybe release, because timeout ", key);
return false;
}
int after = ++vo.count;
log.debug("acquire lock {} {} ", key, after);
return true;
}
/**
* 释放锁
*
* @param key redis key
*/
private void release(String key) {
Map<String, LockVO> map = lockMap.get();
if (map == null || map.size() == 0 || !map.containsKey(key)) {
return;
}
LockVO vo = map.get(key);
if (vo.afterExpireTime < System.currentTimeMillis()) {
log.debug("release lock {}, because timeout ", key);
map.remove(key);
return;
}
int after = --vo.count;
log.debug("release lock {} {} ", key, after);
if (after > 0) {
return;
}
map.remove(key);
RedisCallback<Boolean> callback = (connection) ->
connection.eval(UNLOCK_LUA.getBytes(StandardCharsets.UTF_8), ReturnType.BOOLEAN, 1,
(key).getBytes(StandardCharsets.UTF_8), vo.lockId.getBytes(StandardCharsets.UTF_8));
redisTemplate.execute(callback);
}
/**
* @param key 锁的key
* @param expire 锁的超时时间 秒
* @param lockId 获取锁后,UUID生成的唯一ID
* @return if true success else fail
*/
private boolean tryLock(String key, long expire, String lockId) {
RedisCallback<Boolean> callback = (connection) ->
connection.set((key).getBytes(StandardCharsets.UTF_8),
lockId.getBytes(StandardCharsets.UTF_8), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
return (Boolean) redisTemplate.execute(callback);
}
private static class LockVO {
/**
* 锁重入的次数
*/
private int count;
/**
* 获取锁后,UUID生成的唯一ID
*/
private String lockId;
/**
* 获取锁之前的时间戳
*/
private long beforeExpireTime;
/**
* 获取到锁的时间戳
*/
private long afterExpireTime;
LockVO(int count, String lockId, long beforeExpireTime, long afterExpireTime) {
this.count = count;
this.lockId = lockId;
this.beforeExpireTime = beforeExpireTime;
this.afterExpireTime = afterExpireTime;
}
}
}
zookeeper 实现分布式锁
待续