分布式锁笔记

单体应用锁的局限性

JDK提供的锁都是在一个JVM下起作用的,也就是在一个tomcat下是没问题的。当存在两个或两个以上的tomcat时,大量的并发请求分散到不同的tomcat上,每一个tomcat中是可以反正并发的产生,但是多个tomcat之间,每个tomcat中获得锁的这个请求,又产生了并发,会出现并发问题。单体应用锁的局限性是只能在一个JVM内加锁,而不能从这个应用层面加锁。

分布式锁

分布式锁就是可以跨越多个JVM、跨越多个进程的锁。分布式锁都是通过第三方组件来实现的。目前比较流行的分布式解决方案有:

  • 数据库,通过数据库可以实现分布式锁,但在高并发的情况下对数据库的压力比较大,所以很少使用。
  • Redis,借助Redis也可以实现分布式锁。Redis的Java客户端种类很多,使用的方法也不同。
  • Zookeeper:借助zookeeper瞬时节点。

基于数据库实现分布式锁

实现步骤:

多个进程、多个线程访问共同的组件数据库。
通过select … for update 访问同一条数据库。
for update 锁定数据,其他线程只能等待。

SELECT @@autocommit;
// 手动提交事务 
SET @@autocommit=0;
SELECT * FROM distribute_lock WHERE lock_key = 'demo' for update;
COMMIT;

优缺点

优点:简单方便、易于理解、易于操作
缺点:并发量大时,对数据库的压力较大。可以将锁的数据库与业务数据库分开

基于Redis实现分布式锁

基于redis Setnx 实现
实现原理:利用NX的原子性,多个线程并发时,只有一个线程可以设置成功。
1、获取锁的redis命令

SET resource_name my_random_value NX PX 300000

resource_name:资源名称,可根据不同的业务区分不同的锁。
my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验。
NX:key不存在时设置成功。
PX/EX:过期时间,出现异常时,锁可以过期失效。
2、释放锁采用Redis的delete命令,释放时需要校验之前设置的随机数,相同才能释放。为了保证这一操作的原子性,使用LUA脚本,LUA脚本如下:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

3、Redis的一个客户端Redisson:
请添加图片描述看门狗机制:
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。
这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题。Redisson给出了自己的答案,就是 watch dog 自动延期机制。
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
看门狗机制相关博客链接:https://www.cnblogs.com/jelly12345/p/14699492.html
加锁的代码:

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
             // 如果锁不存在,则设置值和过期时间
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return nil; " +
                    "end; " +
                    // 如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1
                    "if (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; " +
                    // 如果锁已存在,但非本线程,则返回过期时间ttl
                    "return redis.call('pttl', KEYS[1]);",
            Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

解锁的代码:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 如果锁的线程和已存在锁的线程不是同一个线程,返回null
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                    "end; " +
                    // 通过hincrby递减1的方式,释放一次锁
                    //若剩余次数大于0 ,则刷新过期时间
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                    "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                    "else " +
                    //否则证明锁已经释放,删除key并发布锁释放的消息
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return nil;",
            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

基于Zookeeper实现分布式锁

Zookeeper的观察器:可以监测某个节点的变化,3个方法:getData(),getChildren(),exists()
实现原理:

  • 利用Zookeeper的瞬时有序节点的特性
  • 多线程并发创建瞬时节点时,得到有序的序列
  • 序号最小的线程获得锁
  • 其他的线程则监听自己序号的前一个序号
  • 前一个线程执行完成,删除自己的序号的节点
  • 下个序号的线程得到通知,继续执行
package com.lank;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * @author lank
 * @since 2022-03-07 20:04
 */
public class ZkLock implements Watcher {

    private final ZooKeeper zooKeeper;

    private String zNode;

    public ZkLock() throws IOException {
        this.zooKeeper = new ZooKeeper("localhost:2181", 50000000, this);
    }

    public boolean getLock(String lockCode) throws Exception {
        try {
            // 判断根节点是否存在
            Stat stat = zooKeeper.exists("/" + lockCode, false);
            if (Objects.isNull(stat)) {
                zooKeeper.create("/" + lockCode,
                        lockCode.getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }
            // 创建瞬时有序节点
            zNode = zooKeeper.create("/" + lockCode + "/" + lockCode + "_",
                    lockCode.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    // 瞬时有序节点
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println("zNode:" + zNode);
            // 判断该节点是不是序号最小的节点
            List<String> nodeChildren = zooKeeper.getChildren("/" + lockCode, false);
            // 排序-升序
            Collections.sort(nodeChildren);
            String firstNode = nodeChildren.get(0);
            if (zNode.endsWith(firstNode)) {
                // 成功获取锁
                return true;
            }
            // 没获取锁则监听前一个节点
            String lastNode = firstNode;
            for (String node : nodeChildren) {
                if (zNode.endsWith(node)) {
                    // 这里有监听,节点消失进入process()方法
                    Stat exists = zooKeeper.exists("/" + lockCode + "/" + lastNode, true);
                    break;
                } else {
                    lastNode = node;
                }
            }

            synchronized (this) {
                wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close();
        }
        return false;
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (watchedEvent.getType().equals(Event.EventType.NodeDeleted)) {
            synchronized (this) {
                notify();
            }
        }
    }

    public void close() throws Exception {
        zooKeeper.delete(zNode, -1);
        zooKeeper.close();
        System.out.println("我已经释放了锁");
    }
}

分布式锁的对比

方式优点缺点
数据库实现简单、易于理解对数据压力大
Redis易于理解自己实现,不支持阻塞,不能续租
Zookeeper支持阻塞需理解Zookeeper、程序复杂
Curator提供锁的方法依赖Zookeeper,强一致性
Redission提供锁的方法,可阻塞
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值