java 分布式锁

  在java的分布式系统中,有时候会出现不同的服务操作同一个资源的情况,如交易系统和充值系统都要操作用户账户,分布式锁为解决分布式系统中多个应用同时访问同一个资源的问题。

java的分布式锁主要有三种实现方式:

  1. 基于数据库的分布式锁
  2. 基于缓存的分布式锁
  3. 基于zookeeper的分布式锁

下面对这三种方式具体分析一下

1.基于数据库的分布式锁

数据库实现分布式锁方式比较多,如悲观锁(查询时增加for update)、乐观锁(通过version字段)、增加一个表记录锁信息等。因为依赖于数据库,比较好理解,但是也存在一些问题。
如悲观锁在某些情况下可能会锁表而不是锁行,乐观锁可能需要多次重试,以及操作数据库的性能开销等等,所以基于数据库的分布式锁不做过多研究,因为我看来基本上不会用到。

2.基于缓存(以redis为例)的分布式锁

相对于基于数据库的分布式锁,基于缓存的分布式锁性能上要好很多,以redis为例,下面用两种方式去实现分布式锁。

  1. 自定义操作
/**
     * 以阻塞方式的获取锁
     *
     * @param key key
     * @param value value
     * @param lockTimeout 锁超时时间
     * @param getTimeout 获取锁超时时间
     * @return
     */
    public boolean lockBlock(String key, String value, long lockTimeout, long getTimeout, TimeUnit timeUnit) {
        long start = System.currentTimeMillis();
        while (true) {
            //检测是否超时
            if (System.currentTimeMillis() - start > getTimeout) {
                log.error("get lock timeout");
                return false;
            }
            //执行set命令
            //1
            Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, value, lockTimeout, timeUnit);          
            if (absent == null) {
                log.error("get lock absent is null");
                return false;
            }
            //是否成功获取锁
            if (absent) {
                return true;
            } else {
                log.info("get lock fail:{},{}",key,value);
            }
        }
    }

    public boolean unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);

        Object result = redisTemplate.execute(redisScript, Collections.singletonList(key),value);
        if(RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        log.error("unlock error");
        return false;

    }

使用方式如下:

@RequestMapping(value = "/testRedis")
    public void testRedis(String key) throws ExecutionException, InterruptedException {

        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            String value = UUID.randomUUID().toString();
            try {
                if (redisUtil.lockBlock(key, value, 3L, 10L, TimeUnit.SECONDS)) {
                    log.info("线程1获取锁成功,value is {}", value);
                    Thread.sleep(2000);
                } else {
                    log.info("线程1获取锁失败,value is {}", value);
                }
            } catch (InterruptedException e) {

                log.info("线程1获取锁异常,value is {}", value);
            } finally {
                if (redisUtil.unlock(key, value)) {
                    log.info("线程1释放锁,value is {}", value);
                }
            }
        }, threadPoolTaskExecutor);

        CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
            String value = UUID.randomUUID().toString();
            try {
                if (redisUtil.lockBlock(key, value, 3L, 10L, TimeUnit.SECONDS)) {
                    log.info("线程2获取锁成功,value is {}", value);
                    Thread.sleep(2000);
                } else {
                    log.info("线程2获取锁失败,value is {}", value);
                }
            } catch (InterruptedException e) {
                log.info("线程2获取锁异常,value is {}", value);
            } finally {
                if (redisUtil.unlock(key, value)) {
                    log.info("线程2释放锁,value is {}", value);
                }
            }
        }, threadPoolTaskExecutor);

        CompletableFuture.allOf(completableFuture1, completableFuture2).get();
    }

这种方式,要注意几个问题:

  1. 为避免程序异常造成解锁操作失败(如断电等异常情况),造成死锁,需要给锁增加超时时间
  2. 为避免加锁成功设置超时时间失败造成死锁,需要保证加锁和设置超时时间是一个原子操作,所以加锁使用setIfAbsent方法
  3. 比保证解锁时是自己持有的锁才可以解,需要对比value,这个要保证唯一

当前的这种方式,还是有一些问题
没有办法保证在操作结束前锁会不会因为超时被释放
不可重入
要简单解决这些问题,可以使用Redisson提供的分布式锁解决方案:
配置如下:

@Value("${spring.redis.host}")
    String redisHost;

    @Value("${spring.redis.port}")
    String redisPort;

    @Value("${spring.redis.password}")
    String redisPassword;

    @Value("${spring.redis.timeout}")
    Integer redisTimeout;

    /**
     * Redisson配置
     * @return
     */
@Bean
    RedissonClient redissonClient() {
        //1、创建配置
        Config config = new Config();

        redisHost = redisHost.startsWith("redis://") ? redisHost : "redis://" + redisHost;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(redisHost + ":" + redisPort)
                .setTimeout(redisTimeout);

        if (StringUtils.isNotBlank(redisPassword)) {
            serverConfig.setPassword(redisPassword);
        }

        return Redisson.create(config);

    }

使用:

@RequestMapping(value = "/testRedisson", method = RequestMethod.POST)
    public void testRedisson(String key) throws ExecutionException, InterruptedException {

        RLock lock = redissonClient.getLock(key);
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {

            try {
                if (lock.tryLock(10, 3, TimeUnit.SECONDS)) {
                    log.info("线程1获取锁成功");
                    Thread.sleep(2000);
                } else {
                    log.info("线程1获取锁失败");
                }
            } catch (InterruptedException e) {
                log.info("线程1获取锁异常");
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    log.info("线程1释放锁");
                }
            }
        }, threadPoolTaskExecutor);

        CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
            try {
                if (lock.tryLock(10, 3, TimeUnit.SECONDS)) {
                    log.info("线程2获取锁成功");
                    Thread.sleep(2000);
                } else {
                    log.info("线程2获取锁失败");
                }
            } catch (InterruptedException e) {
                log.info("线程2获取锁异常");
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    log.info("线程2释放锁");
                }
            }
        }, threadPoolTaskExecutor);

        CompletableFuture.allOf(completableFuture1, completableFuture2).get();

    }

Redisson通过lua脚本判断实现了锁的重入,以及watch dog机制实现了刷新锁的过期时间
watch dog机制每过1/3超时时间会去判断当前持有锁的线程是否还没有完成逻辑,如果没有,那么会刷新过期时间,这个过期时间默认是30s,可以通过lockWatchDogTimeout来修改。只有在没有显式的设置加锁时间时,watch dog机制才会生效,像上面的例子中,因为设置了失效时间,所以watch dog机制不会刷新锁的过期时间。

3.基于zookeeper的分布式锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

  1. 创建一个锁目录 /locks,该节点为持久节点

  2. 想要获取锁的线程都在锁目录下创建一个临时顺序节点

  3. 获取锁目录下所有子节点,对子节点按节点自增序号从小到大排序

  4. 判断本节点是不是第一个子节点,如果是,则成功获取锁,开始执行业务逻辑操作;如果不是,则监听自己的上一个节点的删除事件

  5. 持有锁的线程释放锁,只需删除当前节点即可。

  6. 当自己监听的节点被删除时,监听事件触发,则回到第3步重新进行判断,直到获取到锁。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

配置如下:

@Slf4j
public class CuratorClientUtil {
    private String zookeeperServer;

    @Getter
    private CuratorFramework client;

    public CuratorClientUtil(String zookeeperServer) {
        this.zookeeperServer = zookeeperServer;
    }

    // 创建CuratorFrameworkFactory并且启动
    public void init() {
        // 重试策略,等待1s,最大重试3次
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
        this.client = CuratorFrameworkFactory.builder()
                .connectString(zookeeperServer)
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        this.client.start();
    }

    // 容器关闭,CuratorFrameworkFactory关闭
    public void destroy() {
        try {
            if (Objects.nonNull(getClient())) {
                getClient().close();
            }
        } catch (Exception e) {
            log.info("CuratorFramework close error=>{}", e.getMessage());
        }
    }
}
@Configuration
public class CuratorConfig {

    @Value("${zookeeper.server}")
    String server;

    @Bean(name = "curatorClientUtil", initMethod = "init", destroyMethod = "destroy")
    public CuratorClientUtil curatorClientUtil() {
        CuratorClientUtil clientUtil = new CuratorClientUtil(server);
        return clientUtil;
    }
}

使用:

@RequestMapping(value = "/testZookeeper", method = RequestMethod.POST)
    public void testZookeeper() throws ExecutionException, InterruptedException {

        InterProcessMutex mutex = new InterProcessMutex(curatorClientUtil.getClient(), lockPath);

        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            try {
                if (mutex.acquire(3L, TimeUnit.SECONDS)) {
                    log.info("线程1获取锁成功");
                    Thread.sleep(5000);
                } else {
                    log.info("线程1获取锁失败");
                }
            } catch (Exception e) {
                log.info("线程1获取锁异常");
                throw new RuntimeException(e);
            } finally {
                try {
                    if (mutex.isOwnedByCurrentThread()) {
                        mutex.release();
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }, threadPoolTaskExecutor);

        CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
            try {
                if (mutex.acquire(3L, TimeUnit.SECONDS)) {
                    log.info("线程2获取锁成功");
                    Thread.sleep(5000);
                } else {
                    log.info("线程2获取锁失败");
                }
            } catch (Exception e) {
                log.info("线程2获取锁异常");
                throw new RuntimeException(e);
            } finally {
                try {
                    if (mutex.isOwnedByCurrentThread()) {
                        mutex.release();
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                log.info("线程2释放锁");
            }
        }, threadPoolTaskExecutor);
        CompletableFuture.allOf(completableFuture1, completableFuture2).get();
    }

总结

相比较而言,基于zookeeper的分布式锁在可靠性上最优,性能也优于数据库,略低于基于缓存的分布式锁,理论上是最佳解决方案。
但我的观点是实际开发中,可能zookeeper并不是很常用,如果单纯为了分布式锁而搭建一套zookeeper集群,似乎并不划算,所以我站redis。

项目地址

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Java分布式锁实现方式有多种,常见的包括: 1. 基于Redis分布式锁:利用Redis单线程的特性,使用SETNX命令创建锁,利用EXPIRE设置锁的过期时间,同时使用DEL命令释放锁,确保锁的释放是原子的。 2. 基于Zookeeper的分布式锁:通过创建临时节点实现分布式锁,当某个服务占用了锁,其它服务将无法创建同名节点,从而保证同一时间只有一个服务占用该锁。 3. 基于数据库分布式锁:使用数据库表中的一行记录来表示锁状态,使用事务确保锁的获取和释放是原子的。 4. 基于Redisson的分布式锁Redisson是一个开源的Java分布式框架,提供了对分布式锁的支持,使用SETNX和EXPIRE命令实现锁的创建和过期,同时还提供了自旋锁、可重入锁等高级特性。 以上是Java分布式锁实现方式的几种常见方式,不同的实现方式有着各自的特点和适用场景,需要根据实际需求进行选择。 ### 回答2: Java分布式锁分布式系统中实现数据同步和控制的关键技术之一,它用于保证多个分布式进程并发访问共享资源时的数据一致性和安全性。分布式锁与普通的锁相比,需要解决跨进程、跨节点的同步和并发控制问题。 Java分布式锁的实现方式有以下几种: 1. 基于Zookeeper实现分布式锁 Zookeeper是一个高性能的分布式协调服务,它可以被用来实现分布式锁。Zookeeper的实现原理是基于它的强一致性和顺序性,可以保证多个进程访问同一个分布式锁时的数据同步和控制。 通过创建一个Zookeeper的持久节点来实现分布式锁,使用create()方法来创建节点,如果创建成功则说明获取锁成功。当多个进程同时请求获取锁时,只有一个进程能够创建节点成功,其它进程只能等待。当持有分布式锁的进程退出时,Zookeeper会自动删除对应的节点,其它进程就可以继续请求获取锁。 2. 基于Redis实现分布式锁 Redis是高性能的内存数据库,可以使用它的setnx()命令来实现分布式锁。setnx()命令可以在指定的key不存在时设置key的值,并返回1;如果key已经存在,则返回0。通过这个原子性的操作来实现分布式锁。 当多个进程同时请求获取锁时,只有一个进程能够成功执行setnx()命令,其它进程只能等待。进程在持有锁期间,可以利用Redis的expire()命令来更新锁的过期时间。当持有分布式锁的进程退出时,可以通过delete()命令来删除锁。 3. 基于数据库实现分布式锁 数据库通过ACID特性来保证数据的一致性、并发性和可靠性,可以通过在数据库中创建一个唯一索引来实现分布式锁。当多个进程同时请求获取锁时,只有一个进程能够成功插入唯一索引,其它进程只能等待。当持有分布式锁的进程退出时,可以通过删除索引中对应的记录来释放锁。 不同的实现方式各有优劣。基于Zookeeper的实现方式可以保证分布式锁的一致性和可靠性,但是需要引入额外的依赖;基于Redis可以实现较高性能的分布式锁,但是在高并发条件下可能会存在死锁等问题;基于数据库的实现方式简单,但在高并发条件下也可能会有锁争抢等问题。 总之,在选择分布式锁的实现方式时,需要根据业务场景和需求来综合考虑各种因素,选择最适合自己的方式。 ### 回答3: 分布式系统中的并发控制是解决分布式系统中竞争资源的重要问题之一,而分布式锁作为一种并发控制工具,在分布式系统中被广泛采用。Java作为一种常用的编程语言,在分布式锁的实现方面也提供了多种解决方案。下面就分别介绍Java分布式锁的实现方式。 1. 基于ZooKeeper的分布式锁 ZooKeeper是分布式系统中常用的协调工具,其提供了一套完整的API用于实现分布式锁。实现分布式锁的过程中需要创建一个Znode,表示锁,同时用于控制数据的访问。在这个Znode上注册监听器用于接收释放锁的成功/失败事件,从而控制加锁/解锁的过程。 2. 基于Redis分布式锁 Redis作为一种高性能的Key-Value数据库,其提供了完整的API用于实现分布式锁。实现分布式锁的过程中需要在Redis中创建一个Key,利用Redis的SETNX命令进行加锁,同时设置过期时间保证锁的生命周期。在解锁时需要判断是否持有锁并删除对应的Key。 3. 基于数据库分布式锁 数据库作为分布式系统中常用的数据存储方式,其提供了事务机制用于实现分布式锁。在实现分布式锁的过程中需要在数据库中创建一个表,利用数据库的事务机制实现加锁/解锁,同时需要设置过期时间保证锁的生命周期。 总之,以上三种方式都是常用的Java分布式锁的实现方式。选择合适的方法需要综合考虑锁的使用场景、性能需求、可靠性要求等因素。同时,在实现分布式锁的过程中需要注意锁的加锁/解锁的正确性和过期时间的设置,保证分布式系统的并发控制的正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白效正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值