0. zookeeper 和 mysql 实现只提供思路
1.分布式锁的要求
1. 独占性 - 任何时刻只能有且只有一个线程持有
2. 高可用 - 高并发请求下依然能使用
3. 防死锁 - 超时机制或者撤销机制
4. 不乱抢 - 只能自己加锁自己释放
5. 重入性 - 可再次获取这个锁
2. 工厂模式
定义一个用于创建对象的接口(也称为工厂接口),但让子类决定实例化哪一个类。工厂模式旨在隐藏创建逻辑,并使系统易于扩展,同时使代码对对象创建的过程解耦
@Component
public class DistributeLockFactory {
@Resource
private RedisTemplate redisTemplate;
@Value("${system.lock.type}")
private String lockType;
private String uuid;
private DistributeLockFactory(){
this.uuid = IdUtil.simpleUUID();
}
public DistributeLock getLock(String lockName,Long expirTime){
switch (this.lockType.toLowerCase()) {
case "redis" : return new RedisDistributeLock(redisTemplate, lockName,uuid, expirTime);
case "mysql" : return new MysqlDistributeLock(lockName,uuid, expirTime);
case "zookeeper" : return new ZookeeperDistributeLock(lockName,uuid, expirTime);
default : throw new IllegalArgumentException("Unsupported lock type: " + lockType);
}
}
}
public interface DistributeLock {
boolean lock();
void unlock();
}
2. redis + lua 脚本实现
public class RedisDistributeLock implements DistributeLock {
/** redis 服务 */
private RedisTemplate redisTemplate;
/** 锁名 */
private String lockName;
/** 线程key */
private String threaKey;
/** 过期时间 */
private Long expirTime;
@Value("${system.lock.max-overtime-num}")
private Integer MAX_NUM = 10;
public RedisDistributeLock(RedisTemplate redisTemplate, String lockName,String uuid,Long expirTime) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.expirTime = expirTime;
this.threaKey = uuid + ":" + Thread.currentThread().getId();
}
@Override
public boolean lock() {
String luaScript = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
while (!(Boolean) redisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(lockName),threaKey,String.valueOf(expirTime))){
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { throw new RuntimeException(e); }
}
// 自动加时
renewExpire(0);
return true;
}
private void renewExpire(int num) {
String lusScript = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then reruen redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
// 最大加时次数
if (num <= MAX_NUM){
return;
}
int finalNum = num++;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if((Boolean) redisTemplate.execute(new DefaultRedisScript(lusScript,Boolean.class),Arrays.asList(lockName),threaKey,String.valueOf(expirTime))) {
renewExpire(finalNum);
}
}
},this.expirTime * 1000 / 3);
}
@Override
public void unlock() {
String luaScript = "\n" +
"if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end";
Long flag = (Long) redisTemplate.execute(new DefaultRedisScript(luaScript, Boolean.class), Arrays.asList(lockName), Arrays.asList(threaKey));
if (null == flag){
throw new RuntimeException("nulock err,maybe not exist this lock?");
}
}
}
3. Zookeeper
-
创建锁节点: 所有想要获取锁的客户端都会在ZooKeeper的一个特定的路径(例如/locks)下创建一个临时的顺序节点。这个节点的名字中会包含一个自增的序列号,这个序列号可以帮助客户端确定节点创建的顺序。
-
判断是否获得锁: 每个客户端在创建节点后,会检查自己创建的节点是否是当前路径下序号最小的节点。如果是,则认为获得了锁;如果不是,则需要等待。
-
监听前一个节点: 如果没有获得锁,客户端会找到比自己创建的节点序号小的那个节点(也就是它前面的一个节点),并对这个节点注册一个Watcher。这样,当这个前驱节点被删除时(意味着持有锁的客户端释放了锁),ZooKeeper会通知正在等待的客户端。
-
获取锁通知: 当接到ZooKeeper的通知后,客户端会再次检查自己是否成为了当前序号最小的节点。如果是,则获取锁;如果不是,则重复步骤3,继续监听它的新前驱节点。
public boolean lock() throws KeeperException, InterruptedException {
lockPath = zk.create(LOCK_ROOT_PATH + "/", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> children = zk.getChildren(LOCK_ROOT_PATH, false);
Collections.sort(children);
// 判断是否是最小的节点
if (lockPath.equals(LOCK_ROOT_PATH + "/" + children.get(0))) {
return true;
} else {
// 监听前一个节点
String prevLockPath = LOCK_ROOT_PATH + "/" + children.get(Collections.binarySearch(children, lockPath.substring(LOCK_ROOT_PATH.length() + 1)) - 1);
Stat stat = zk.exists(prevLockPath, new LockWatcher(latch));
if (stat != null) {
latch.await();
}
}
return true;
}
-
释放锁: 当一个客户端完成操作后,它会删除自己创建的节点,这个操作同时也通知了正在监听它的下一个节点的客户端,使得下一个客户端有机会获取锁。
-
故障处理: 如果某个客户端在等待锁的过程中崩溃,因为它创建的是临时节点,所以当客户端与ZooKeeper的连接断开时,这个节点会被自动删除,这样就自然地释放了锁,并且通知了下一个等待的客户端。
4.mysql
1.创建锁表
CREATE TABLE `distributed_lock` (
`lock_name` VARCHAR(64) PRIMARY KEY NOT NULL,
`thread_key` VARCHAR(64) NOT NULL,
`lock_num` INT(10) NOT NULL,
`state` INT(4) NOT NULL,
`expire` TIMESTAMP NULL
);
2. 加锁
INSERT INTO distributed_lock (lock_name, thread_key,lock_num,state, expire)
VALUES ('lock_key', 'thread_key',1,1, DATE_ADD(NOW(), INTERVAL 5 MINUTE))
ON DUPLICATE KEY UPDATE state = 1 ,lock_num = lock_num+1, expire = DATE_ADD(NOW(), INTERVAL 5 MINUTE);
这里使用了ON DUPLICATE KEY UPDATE来确保当锁已经存在时更新锁的状态,增加加锁次数,过期时间,避免锁被无限期占用。