分布式锁的各种实现及优缺点

应用场景

  • 超卖现象
    • 解决办法
      • update的行锁
      • Synchronized关键字,且手动控制事务
      • 使用ReentrantLock,jdk1.5之后出现

单体下锁的局限性

  • 无法跨JVM进程,跨多应用环境

基于数据库悲观锁的分布式锁

实现步骤

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

注意:需要关闭事务的自动提交。

优缺点

  • 优点:简单方便、便于理解、抑郁操作
  • 缺点:并发量大时,对数据库压力较大
  • 建议:所谓锁的数据库与业务数据库分开

代码实现

  • 从数据库查询锁

    @Select("select * from `lock` where business_code='demo' for update")
    Lock getLock();
    
  • 获取分布式锁,执行方法,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法

    @RestController
    @Slf4j
    public class SqlConreoller {
    
        @Autowired
        private SqlMapper sqlMapper;
    
        @GetMapping("sqllock")
        @Transactional(rollbackFor = Exception.class)
        public String getLock() throws Exception {
            log.info("进入了方法");
            Lock lock = sqlMapper.getLock();
            if(null==lock) throw new Exception("获取不到分布式锁");
            log.info("进入了锁");
    
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return "getlock";
        }
    }
    

基于Redis的Setnx实现分布式锁

实现原理

  • 获取锁的redis命令
  • set resource_name my_random_value NX PX 30000
    • resource_name :资源名称
    • my_random_value :随机值,每个线程的随机值都不相同,用于释放锁时的校验
    • NX :key不存在时设置成功,key存在则设置不成功
    • PX :自动失效时间,出现异常情况,锁可以过期失效
  • 主要是利用NX的原子性,多个线程并发的时候,只有一个线程设置成功
  • 只有获取到锁的线程,才能执行业务
  • 锁还有过期时间,若发生异常,锁到达过期时间会自动失效,其他线程可以继续获取锁
  • 释放锁才有redis的DELETE命令
  • 释放锁是校验之前设置随机数,相同才能释放
  • 释放锁使用LUA脚本

代码实现

  • 分布式锁工具类

    @Slf4j
    public class RedisLock implements AutoCloseable {
        private RedisTemplate redisTemplate;
        private String key;
        private String value;
        //单位:秒
        private int expireTime;
    
        public RedisLock(RedisTemplate redisTemplate, String key, int expireTime) {
            this.redisTemplate = redisTemplate;
            this.key = key;
            this.value = UUID.randomUUID().toString();
            this.expireTime = expireTime;
        }
    
        /**
         * 获取分布式锁
         *
         * @return
         */
        public boolean getLock() {
            RedisCallback<Boolean> redisCallback = connection -> {
                // 设置NX
                RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                // 设置过期时间
                Expiration expiration = Expiration.seconds(expireTime);
                // 序列化key
                byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
                // 序列化value
                byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
                // 执行setnx操作
                Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
                return result;
            };
    
            Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
            return lock;
        }
    
        /**
         * 释放分布式锁
         *
         * @return
         */
        public boolean unLock() {
            // LUA脚本
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
            RedisScript<Boolean> redisScript=RedisScript.of(script,Boolean.class);
            List<String> keys = Arrays.asList(key);
    
            Boolean result =(Boolean) redisTemplate.execute(redisScript, keys, value);
            log.info("释放锁结果:"+result);
    
            return result;
        }
    
        @Override
        public void close() throws Exception {
            unLock();
        }
    }
    
  • 测试分布式锁,未获得锁的线程会直接结束,不会阻塞

    @Slf4j
    @RestController
    public class RedisController {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @GetMapping("redisLock")
        public String getLock() {
            String redisKey = "rediskey";
            log.info("进入了方法");
    
            try (RedisLock redisLock = new RedisLock(redisTemplate, redisKey, 30)) {
                boolean lock = redisLock.getLock();
                if(lock){
                    log.info("进入了锁");
                    Thread.sleep(15000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            log.info("方法执行完成");
            return "redislock";
        }
    }
    

基于Zookeeper的瞬时节点实现分布式锁

Zookeeper数据结构

  • 持久节点
  • 瞬时节点:有序,瞬时节点不可再有子节点,会话结束后瞬时节点会自动消失

Zookeeper的观察器

  • 可设置观察器的3个方法:getData();getChildren();exists();
  • 节点数据发生变化,发送给客户端
  • 观察器只能监控一次,再次监控需要重新设置

实现原理

  • 利用Zookeeper瞬时节点的特性
  • 多线程并发创建瞬时节点,得到有序数列
  • 序号最小的节点得到锁
  • 其他线程则监听自己序号的前一个序号
  • 前一个执行完,删除自己序号的节点
  • 下一个序号节点得到通知,继续执行
  • 以此类推,根据之前确认好的顺序执行

代码实现

  • Zookeeper分布式锁的工具类

    @Slf4j
    public class ZookeeperLock implements AutoCloseable, Watcher {
        private ZooKeeper zooKeeper;
        private String znode;
    
        public ZookeeperLock() throws IOException {
            this.zooKeeper = new ZooKeeper("localhost:2181",
                                           10000, this);
        }
    
        /**
         * 获取锁
         * @param businessCode
         * @return
         */
        public boolean getLock(String businessCode) {
            try {
                //创建业务 根节点
                Stat stat = zooKeeper.exists("/" + businessCode, false);
                if (stat == null) {
                    zooKeeper.create("/" + businessCode, businessCode.getBytes(),
                                     ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                     CreateMode.PERSISTENT);
                }
    
                //创建瞬时有序节点  /order/order_00000001
                znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
                                         ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                         CreateMode.EPHEMERAL_SEQUENTIAL);
    
                //获取业务节点下 所有的子节点
                List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
                //子节点排序
                Collections.sort(childrenNodes);
                //获取序号最小的(第一个)子节点
                String firstNode = childrenNodes.get(0);
                //如果创建的节点是第一个子节点,则获得锁
                if (znode.endsWith(firstNode)) {
                    return true;
                }
                //不是第一个子节点,则监听前一个节点
                String lastNode = firstNode;
                for (String node : childrenNodes) {
                    if (znode.endsWith(node)) {
                        zooKeeper.exists("/" + businessCode + "/" + lastNode, true);
                        break;
                    } else {
                        lastNode = node;
                    }
                }
                synchronized (this) {
                    wait();
                }
                return true;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
    
        /**
         * 释放锁
         * @throws Exception
         */
        @Override
        public void close() throws Exception {
            zooKeeper.delete(znode, -1);
            zooKeeper.close();
            log.info("我已经释放了锁!");
        }
    
        /**
         * 通知下一节点
         * @param event
         */
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                synchronized (this) {
                    notify();
                }
            }
        }
    }
    
  • 使用分布式锁,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法

    @Slf4j
    @RestController
    public class ZookeeperController {
    
        @GetMapping("zookeeperLock")
        public String getLock() {
            log.info("进入方法");
            try(ZookeeperLock zookeeperLock=new ZookeeperLock()) {
                boolean lock = zookeeperLock.getLock("zookeeper");
                if(lock){
                    log.info("进入了锁");
                    Thread.sleep(10000);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            log.info("方法执行完成");
            return "zookeeperLock";
        }
    }
    

基于Zookeeper的Curator客户端实现分布式锁

  • curator客户端已经实现了分布式锁
  • 直接调用即可

代码实现

@Slf4j
@RestController
public class CuratorController {

    @GetMapping("curatorLock")
    public String getLock() {
        log.info("进入了方法");
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
        client.start();
        InterProcessMutex lock=new InterProcessMutex(client,"/order");
        try {
            if(lock.acquire(30, TimeUnit.SECONDS)){
                try {
                    log.info("进入了锁");
                    Thread.sleep(10000);
                }finally {
                    lock.release();
                    log.info("释放了锁");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "curatorLock";
    }
}

基于Redisson实现分布式锁

  • Redisson客户端已经实现了分布式锁
  • 直接调用即可

代码实现

  • 获取Redisson客户端

    @Configuration
    public class RedissonConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port}")
        private String port;
    
        @Bean
        public RedissonClient getRedisson(){
    
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + host + ":" + port);
            //添加主从配置
    //        config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
    
            return Redisson.create(config);
        }
    
    }
    
  • 使用分布式锁,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法

    @RestController
    @Slf4j
    public class RedissonController {
        @Autowired
        private RedissonClient redisson;
    
        @GetMapping("redissonLock")
        public String getLock(){
            log.info("进入方法");
            RLock rLock = redisson.getLock("redissonLock");
    
            try {
                rLock.lock(30, TimeUnit.SECONDS);
                log.info("进入了锁");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("方法执行完成");
            return "redissonLock";
        }
    }
    

分布式锁实现方案优缺点分析

方式优点缺点
数据库实现简单、便于理解对数据库压力大
Redis易于理解自己实现、不支持阻塞
Zookeeper支持阻塞需理解Zookeeper、程序复杂
Curator提供锁的方法依赖Zookeeper,强一致
Redisson提供锁的方法,可阻塞

总结

  • 在项目中不推荐自己编写的分布式锁,平时可以自己编写,提高动手能力
  • 推荐Redisson和Curator实现分布式锁
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis分布式锁和ZooKeeper(简称ZK)都是常见的分布式锁实现方式。它们各自有一些优点和缺点。 Redis分布式锁的优点包括: 1. 简单易用:Redis是一个流行的键值存储系统,使用起来相对简单,支持多种编程语言的客户端库。 2. 高性能:由于Redis存储在内存中,读写速度较快,适用于高并发场景。 3. 可扩展性:通过Redis的主从复制和集群模式,可以实现高可用和扩展性。 Redis分布式锁的缺点包括: 1. 单点故障:当Redis的主节点宕机时,可能会导致锁失效,需要依赖哨兵或集群模式来提高可用性。 2. 无法保证强一致性:由于Redis是一个内存数据库,当出现网络分区或主从同步延迟时,可能会导致数据不一致的情况发生。 3. 锁竞争问题:由于Redis的单线程特性,当并发请求较高时,可能会导致竞争激烈,影响性能。 ZooKeeper分布式锁的优点包括: 1. 强一致性:ZooKeeper是一个分布式协调服务,可以提供强一致性的数据存储和访问。 2. 可靠性:ZooKeeper采用多数投票机制来保证数据一致性,可以在网络分区或节点故障情况下正常运行。 3. 顺序性:ZooKeeper提供有序节点的特性,可以用于实现公平锁。 ZooKeeper分布式锁的缺点包括: 1. 复杂性:ZooKeeper相对于Redis来说使用起来较为复杂,需要依赖ZooKeeper本身的客户端库,并且需要部署和管理ZooKeeper集群。 2. 性能较低:相比Redis的高性能特性,ZooKeeper的性能较低,适用于对一致性要求较高但并发量不大的场景。 综上所述,选择使用Redis分布式锁还是ZooKeeper分布式锁取决于具体业务需求和系统特点。如果对性能要求较高且可以容忍一定的数据不一致性,则可以选择Redis。如果对一致性要求较高且可以接受一定的性能损耗和复杂性,则可以选择ZooKeeper。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值