什么是分布式锁

开发中,常常需要保护一段代码的在同一时刻只能一个线程运行,这段代码称为临界资源,此时需要把临界资源用一把锁锁上,用完了再释放锁给其他线程使用。

  • 单机系统:单机系统在多用户多线程并发操作同一份资源的时候,采用线程加锁的机制,即当某个线程获取到该资源后,立即加锁,当使用完后,再解锁,其它线程就可以接着使用了。
  • 分布式系统:在分布式系统环境中,单机的线程锁机制是不起作用的,因为系统采用了集群部署在不同的机器上;

如果是多用户同时访问同一份资源时,JAVA提供的锁在集群中是起不到作用的,它只能锁住本地JVM的操作。因为资源在不同的服务器之间共享。

分布式锁作用

  • 锁资源

把多用户的并行操作转化为串行操作,如在秒杀系统的高并发减库存操作,为了不出现超买行为,使用分布式锁,减库存把并行操作转化为串行操作,保证库存正确性。

  • 幂等性

利用分布式锁+token模式可以实现接口的幂等性。如果单单是分布式锁,只是锁住资源在这次操作中唯一拥有锁,当锁释放时,如果重复提交的相同请求,如下单请求,因网络问题恰好在锁释放瞬间拥有了锁,还是会出现超买行为,分布式锁只是某一临界资源在某一时刻只有一个线程操作,这时是无法实现幂等性,这需要在临界资源里自己实现相关幂等性逻辑。

分布式锁特点

  1. 互斥性:指不可能同时2个线程以上的人拿到锁。
  2. 可用性:redis集群环境下,不能因为某个节点瘫痪,导致客户端不能获取和释放锁。
  3. 终止性:为了避免死锁,必须有自动的终止或撤销锁操作,一般是采用超时处理机制。
  4. 抢占性:其他线程已经占了锁,不能私下解锁其他线程的锁,必须等锁的释放
  5. 可重入性: 同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。

主流分布式锁实现方案

  • 基于数据库实现:采用数据库的乐观锁(CAS)悲观锁(for update)来实现。
  • 基于ZooKeeper实现:采用它的临时有序节点来实现的分布式锁。
  • 基于Redis实现:依赖redis的原子性操作来实现

三种方式都可以实现分布式锁,如果并发量不大的话,直接采用数据库就可以,如果高并发的话,就要考虑zookeeper或redis,但从高并发高性能角度考虑,基于Redis 实现性能会更好;

所以如何选择,还是取决于业务需求。

基于Redis的setnx实现分布式锁

  1. 采用setnx加锁

语法: setnx key value

nx 是not exist 的意思。

例如:

127.0.0.1:6379> setnx lockkey  value1
(integer) 1
127.0.0.1:6379> setnx lockkey  value1
(integer) 0
复制代码
  • 如果 setnx 返回1,说明拿到锁了。
  • 如果 SETNX 返回0,说明拿锁失败,被其他线程占用。
  1. 为了避免死锁,增加expire超时机制

为该锁增加10分钟超时,防止死锁

127.0.0.1:6379> expire lockkey 600
(integer) 1
复制代码
  1. 原子性操作

因为要操作2个步骤 setnx 和 expire 无法保证锁的原子性操作,用以下来代替:

SET key value [NX] [XX] [EX <seconds>] [PX [millseconds]]

必选参数说明

  • SET:命令
  • key:待设置的key
  • value: 设置的key的value

可选参数说明

  • NX:表示key不存在才设置,如果存在则返回NULL
  • XX:表示key存在时才设置,如果不存在则返回NULL
  • EX seconds:设置过期时间,过期时间精确为秒
  • PX millsecond:设置过期时间,过期时间精确为毫秒

以上set 代替了 setnx +expire 需要分2次执行命令操作的方式,保证了原子性

127.0.0.1:6379> set lockkey  value1  NX px 600000
OK
127.0.0.1:6379> set lockkey value1  NX px 600000
(nil)
复制代码
  • 如果setnx 返回ok 说明拿到了锁
  • 如果setnx 返回 nil,说明拿锁失败,被其他线程占用。

案例实战:基于redis的分布式锁实现

需要串行运行的场景,下面代码是分布式锁,能在分布式环境中保证临界资源的独占性,但不能保证请求操作的幂等性

因为锁释放瞬间可能刚好因为网络延迟问题,客户端重复点击的相同的重复请求刚好到达服务器并获取到锁,还是会走逻辑代码的。

除非请求参数中有全局唯一的参数,经过MD5后key也是唯一的,或者业务中根据唯一key做幂等验证,这个时候分布式锁才能实现幂等性。

关于幂等性问题有时间再单独用一篇文章探索。。。

    @PostMapping(value = "/createOrder")
    public String createOrder(@RequestBody  OrderDTO obj) {

        //步骤1:先转换为唯一MD5
        String json=JsonUtil.object2Json(obj);
        String md5 = DigestUtils.md5DigestAsHex(json.getBytes()).toUpperCase();

        //步骤2:把md5设置为分布式锁的key
        /**
         * setIfAbsent 的作用就相当于 SET key value [NX] [XX] [EX <seconds>] [PX [millseconds]]
         * 设置 5分钟过期
         */
        Boolean do=stringRedisTemplate.opsForValue().setIfAbsent(md5,"1",60*5, TimeUnit.SECONDS);
        if(do){
            // 加锁成功
            log.debug("{}拿锁成功,开始处理业务",md5);
            try {
                //模拟10秒 业务处理
                Thread.sleep(1000*10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //步骤3:解锁
            stringRedisTemplate.delete(md5);
            log.debug("{}拿锁成功,结束处理业务",md5);
            return "ok";
        }else{
            log.debug("{}拿锁失败",md5);
            return  "拿不锁,直接退出!";
        }
    }

复制代码


原文链接:https://juejin.cn/post/7056412227850797086