工厂模式实现三种分布式锁+redis+mysql+zookeeper

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

  1. 创建锁节点: 所有想要获取锁的客户端都会在ZooKeeper的一个特定的路径(例如/locks)下创建一个临时的顺序节点。这个节点的名字中会包含一个自增的序列号,这个序列号可以帮助客户端确定节点创建的顺序。

  2. 判断是否获得锁: 每个客户端在创建节点后,会检查自己创建的节点是否是当前路径下序号最小的节点。如果是,则认为获得了锁;如果不是,则需要等待。

  3. 监听前一个节点: 如果没有获得锁,客户端会找到比自己创建的节点序号小的那个节点(也就是它前面的一个节点),并对这个节点注册一个Watcher。这样,当这个前驱节点被删除时(意味着持有锁的客户端释放了锁),ZooKeeper会通知正在等待的客户端。

  4. 获取锁通知: 当接到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;
    }
  1. 释放锁: 当一个客户端完成操作后,它会删除自己创建的节点,这个操作同时也通知了正在监听它的下一个节点的客户端,使得下一个客户端有机会获取锁。

  2. 故障处理: 如果某个客户端在等待锁的过程中崩溃,因为它创建的是临时节点,所以当客户端与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来确保当锁已经存在时更新锁的状态,增加加锁次数,过期时间,避免锁被无限期占用。

3. 释放锁

1.需要开启事务

2.根据 lock_num 判断是否删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值