Java进阶2-分布式锁

Zookeeper分布式锁

实现方式:
利用 zookeeper 的同级节点的唯一顺序特性,在需要获取排他锁时,所有的客户端试图通过调用 create() 接口,在/exclusive_lock 节点下创建临时子节点 /exclusive_lock/lock,最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。同时,所有没有获取到锁的客户端可以在 /exclusive_lock节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。

Redis的SetNX分布式锁

SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:

SET lock_key unique_value NX PX 10000

# 加锁设置过期时间要保证原子性
lock_key 就是 key 键;
unique_value 是客户端生成的唯一的标识;
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

Redission分布式锁

8.1. 可重入锁(Reentrant Lock)

/**
 * 8.1. 可重入锁(Reentrant Lock)
 * @return
 */
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {

    //1、获取一把锁,只要锁的名字一样,就是同一把锁
    RLock myLock = redisson.getLock("my-lock");

    //2、加锁
    myLock.lock(); //阻塞式等待。默认加的锁都是30s

    //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
    //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
    // myLock.lock(10,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间

    //问题:在锁时间到了以后,不会自动续期
    //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
    //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
    //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
    // internalLockLeaseTime 【看门狗时间】 / 3, 10s

    try {
        System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
        try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
        System.out.println("释放锁..." + Thread.currentThread().getId());
        myLock.unlock();
    }

    return "hello";
}

8.5. 读写锁(ReadWriteLock)

 /**
     * 8.5. 读写锁(ReadWriteLock)
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
        String s = "";
        //获取读写锁接口
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //获取写锁
        RLock rLock = readWriteLock.writeLock();
        try {
            //1、改数据加写锁,读数据加读锁
            rLock.lock();
            s = UUID.randomUUID().toString();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            ops.set("writeValue",s);
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //解锁
            rLock.unlock();
        }

        return s;
    }
    /**
     * 8.5. 读写锁(ReadWriteLock)
     * @return
     */
    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
        String s = "";
        //获取读写锁接口
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //加读锁
        RLock rLock = readWriteLock.readLock();
        try {
            //加锁
            rLock.lock();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            s = ops.get("writeValue");
            try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            rLock.unlock();
        }

        return s;
    }

8.6. 信号量(Semaphore)

 /**
     * 8.6. 信号量(Semaphore)
     * 车库停车
     * 3车位
     * 应用场景:信号量也可以做分布式限流
     */
    @GetMapping(value = "/park")
    @ResponseBody
    public String park() throws InterruptedException {
        //信号量对象
        RSemaphore park = redisson.getSemaphore("park");

        //阻塞方法,获取一个信号、获取一个值,占一个车位
        park.acquire();

        //tryAcquire快速失败抛出
        boolean flag = park.tryAcquire();
        if (flag) {
            //执行业务
        } else {
            return "error";
        }

        return "ok=>" + flag;
    }
    @GetMapping(value = "/go")
    @ResponseBody
    public String go() {
        //信号量对象
        RSemaphore park = redisson.getSemaphore("park");
        //释放一个车位
        park.release();
        return "ok";
    }

8.8. 闭锁(CountDownLatch)

 /**
     * 8.8. 闭锁(CountDownLatch)
     * 放假、锁门
     * 1班没人了
     * 5个班,全部走完,我们才可以锁大门
     * 应用场景:分布式闭锁,JUC也有。在多线程任务调度中使用,比如多个线程任务全部完成才算全部完成。
     */
    @GetMapping(value = "/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        //闭锁对象
        RCountDownLatch door = redisson.getCountDownLatch("door");
        //等待个数
        door.trySetCount(5);
        //等待闭锁完成
        door.await();
        return "放假了...";
    }
    @GetMapping(value = "/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id) {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();       //计数-1
        return id + "班的人都走了...";
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值