分布式锁

在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁

阻塞锁使用一个互斥量来实现:

可以用一个整数表示,或者也可以用某个数据是否存在来表示

数据库唯一索引

获得锁时向表中插入一条记录,释放锁时删除这条记录

redis setnx

1.获取锁的时候,对某个key执行setnx,加锁(如果设置成功(获得锁)返回1,否则返回0),并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2.获取锁的时候还设置一个获取的超时时间(防止死锁),若超过这个时间则放弃获取锁。

3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放

实现

public class RedisLock {
    
    private StringRedisTemplate template;

    private static final String LOCK_KEY = "LOCK";

    private String identifyValue;

    public RedisLock(StringRedisTemplate template) {this.template = template;}

    /**
     * @param acquireTimeout 获取锁之前的超时时间
     * @param expireTime     锁的过期时间
     * @return
     */
    public boolean lock(long acquireTimeout, long expireTime) {
        // 获取锁的时间
        long inTime = System.currentTimeMillis();
        identifyValue = UUID.randomUUID().toString();
        for (; ; ) {
            // 判断获取锁是否超时
            if (System.currentTimeMillis() - inTime >= acquireTimeout) {
                return false;
            }
            // 通过setnx的方式来获取锁
            if (template.opsForValue().setIfAbsent(LOCK_KEY, identifyValue, expireTime, TimeUnit.MILLISECONDS)) {
                // 获取锁成功
                return true;
            }
            // 获取锁失败,继续自旋
        }
    }

    public void release() {
        if (identifyValue == null){
            throw new IllegalStateException("没有获取锁");
        }
        // 删除的时候验证value,必须确保释放的锁是自己创建的
        if (!identifyValue.equals(template.opsForValue().get(LOCK_KEY))){
            throw new IllegalStateException("锁的value不一致");
        }
        template.delete(LOCK_KEY);
    }
}

与zookeeper比较

相对比来说Redis比Zookeeper性能要好,从可靠性角度分析,Zookeeper可靠性比Redis更好。因为Redis有效期不是很好控制,可能会产生有效期延迟

redis redlock

使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用

计算获取锁消耗的时间,只有消耗的时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功 如果获取锁失败,就到每个实例上释放锁

zookeeper临时节点

多个进程同时在zookeeper.上创建同一个相同的节点(/lock) , 因为zookeeper节点是唯一的,如果是唯一的话,那么同时如果有多个客户端创建相同的节点/lock的话,最终只有看谁能够快速的抢资源,谁就能创建/lock节点,创建节点不成功的进程,会注册一个监听事件,等节点被删除的时候,重新竞争这个锁 这个时候节点类型应该使用临时类型。

当一个进程释放锁后(关闭zk连接或者会话超时),临时节点会被删除,等待锁的其他进程会收到节点被删除的通知,这些等待的进程会重新参与到竞争

需要注意的是,要根据业务设置锁等待时间,避免死锁

实现

public void lock() {
    // 尝试获取锁,如果成功,就真的成功了
    if (tryLock()) {
        System.out.println(Thread.currentThread().getName() + "获取锁成功");
    // 否则等待锁
    } else {
        waitLock(); 
        // 当等待被唤醒后重新去竞争锁
        lock();
    }
}
private boolean tryLock() {
    try {
        // 通过zk创建临时节点的成功与否来表示是否获得锁
        zkClient.createEphemeral("/lock");
        return true;
    } catch (Exception e) {
        return false;
    }
}
private void waitLock() {
    // 监听节点被删除的事件
    zkClient.subscribeDataChanges("/lock", new IZkDataListener() {
        @Override
        public void handleDataDeleted(String s) throws Exception {
            // 如果节点被删除,唤醒latch
            if (latch != null) {
                latch.countDown();
            }
        }
    });
    // 如果zk有lock这个锁
    if (zkClient.exists("/lock")) {
        // 在这里进行等待,直至被上面的事件监听唤醒
        latch = new CountDownLatch(1);
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    // 等待完成删除所有监听事件,避免监听器堆积影响性能
    zkClient.unsubscribeAll();
}
public void release() {
    if (zkClient != null) {
        // 关闭zk客户端,临时节点也随之被删除,相当于释放锁,让其他人去竞争
        zkClient.close();
        System.out.println(Thread.currentThread().getName()+"释放锁完成");
    }
}

zookeeper临时顺序节点

有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁

zk锁 vs redis锁

分布式Session

集群产生的问题

服务器集群后,因为session是存放在服务器上,客户端会使用同一个Sessionid在多个不同的服务器上获取对应的Session,从而会导致Session不一致问题

解决方案

SpringSession 使用redis存储session

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
@EnableRedisHttpSession

这时候,Session的存取都是通过redis来了