分布式锁详解及实现案例

前言

随着互联网的发展,人们网上购物已然成为常态,特别是双十一和618等大型的购物节,网站的并发数量也急剧上升,因此我们后台的框架也逐渐从以前的单机版升级到现在的分布式集群。这一切的演变最终目的都是为了提升系统性能,给用户带来更好的购物体验。在曾今的单机环境下多线程的并发抢夺资源的情况我们用SynchronizedReentrantLock都可以完美的解决,但是由于现在的环境是同一个服务有多个节点,当同一个服务的多个节点同时操作某一个公共资源时我们又该如何去解决这个问题呢?这就是提到过的分布式锁的问题

一、什么是分布式锁

首先分布式锁的这个概念只有在分布式集群环境下才有,单机版的系统中不存在这个概念。在单机版的系统中我们为解决多线程抢夺共享资源的问题引入了Synchronized和ReentrantLock两种单机锁,那么分布式锁其实也是一个道理,是一种专门解决分布式集群环境下多个jvm抢夺共享资源的一种处理手段。下面举个比较恶心的例子来说明单机锁和分布式锁:
前提:假如一层楼只有一个厕所并且只有一个坑位,每次只能去一个人
单机锁:

该层楼只有一个办公室,为了防止多人抢夺坑位,就给办公室的大门上了一把锁,并且锁只有一把钥匙,因此需要上厕所就必须拿到办公室的钥匙才可以。(注意此时上锁的对象是办公室的大门)

分布式锁:

该楼层有多个办公室,如果依旧采用单机锁也就是给每个办公室的都上一把锁,此时会发现还是会出现抢占坑位的情况,因为每个办公室是独立的。因此这种情况我们就需要给厕所门上一把锁,让所有办公室的人去抢夺这个厕所门的锁,而不是给每个办公室的门上一把锁,如此我们才能解决多个办公室场景下的抢夺资源问题。

以上案例中,厕所就是我们需要抢夺的资源,而办公室就是我们程序运行的jvm,办公室中的每个人就是每个jvm中运行的线程。

二、分布式锁应该具备哪些条件

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三、如何实现分布式锁

分布式锁的实现常用方式有如下三种:

  • MySQL数据库实现:基于数据库的乐观锁实现(工作中不推荐使用)

  • Redis数据库实现:基于Redisson实现、基于Lua脚本实现(推荐)

  • Zookeeper数据:基于Zookeeper中不能创建相同的临时节点二实现

四、Redis实现分布式锁(推荐)

1.实现流程

①获取锁
②执行业务逻辑
③释放锁
其实我们分布式锁中主要解决的问题就是上锁和解锁过程的原子性,只要保证了上锁和解锁过程的原子性以及保证锁在异常情况下锁能够得到释放。

2.搭建基础框架环境(springboot+redis+mysql+MybatisPlus)
2.实现方式

用lua脚本实现:

 /** 添加分布式锁,用lua脚本方式 */
  private String getDataByLua(Long userId) {
    // 抢占分布式锁
    String value = UUID.randomUUID().toString().replaceAll("-", ""); // 当前锁的指
    Boolean lock =
        redisTemplate
            .opsForValue()
            .setIfAbsent(ConstantUtil.REDIS_USER_LOCK_KEY + userId, value, 300, TimeUnit.SECONDS);
    String fromDb = "";
    if (lock) {
      System.out.println("获取分布式锁成功!");
      try {
        fromDb = getDataFromDb(userId);
      } finally {
        // 删除分布式锁,传入value,确保删除的是自己的锁
        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";
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), value);
      }
    } else {
      System.out.println("获取分布式锁失败,等待重试!");
      try {
        Thread.sleep(200);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      return getDataByLua(userId);
    }
    return fromDb;
  }

Redisson实现:

  private String getDataByRedison(Long userId) {
    RLock lock = redissonClient.getLock(ConstantUtil.REDIS_USER_LOCK_KEY + userId);
    // 阻塞式等待,默认锁为30S,业务执行期间看门狗会自动续锁,业务执行完如果不手动删除锁,30S后自动删除
    lock.lock();
    String fromDb = "";
    try {
      System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
      fromDb = getDataFromDb(userId);
    } finally {
      System.out.println("释放锁"+Thread.currentThread().getId());
      lock.unlock();
    }
    return fromDb;
  }

五、相关资料

Redis分布式锁官方汉化版文档:http://www.redis.cn/topics/distlock.html
本示例demo链接:www.baidu.com
Redisson源码教程链接:https://github.com/redisson/redisson

六、小结

我自己只研究过用redis来实现,在项目中也看到老大用redis来实现的,不过他是用lua脚本的方式实现的,至于为什么不用redisson来实现我也不得而知,其实两种方法都是一样的,因为redisson的底层其实也是通过lua脚本来实现的。至于mysql和zookeeper的实现方式,大家也可以去学习一下。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java中的Lock接口是Java中并发编程的一种实现方式,它提供了比传统的synchronized块更高级别的锁定机制。 Lock接口的使用场景如下: 1. 多个线程同时访问共享资源时,需要确保同一时刻只有一个线程可以访问该资源。 2. 当线程需要访问一个共享资源时,需要先获取该资源的锁。如果该资源已经被其他线程锁定,则当前线程会被阻塞,直到获取到该资源的锁为止。 3. 当线程访问完共享资源后,需要释放该资源的锁,以便其他线程可以继续访问该资源。 在Java中,Lock接口的主要实现类是ReentrantLock。使用Lock接口可以实现更细粒度的锁控制,比如可以指定锁定的超时时间、可重入性等。 下面是Lock接口的使用示例: ``` import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockExample { private final Lock lock = new ReentrantLock(); public void doSomething() { lock.lock(); try { // 执行需要锁定的操作 } finally { lock.unlock(); } } } ``` 在上述代码中,ReentrantLock实例被用来保护某个需要锁定的操作,doSomething()方法获取锁之后执行需要锁定的操作,最后释放锁。 总之,使用Lock接口可以更加灵活地控制多线程并发访问共享资源的行为,从而提高程序的并发性能。 ### 回答2: 在Java分布式环境中,Lock接口可以用于解决多个线程之间的并发访问问题,并确保资源的互斥访问。Lock接口的使用场景如下: 1. 多线程编程:在多线程编程中,如果多个线程同时访问共享资源,可能会导致数据的不一致性或竞态条件的出现。通过使用Lock接口,可以保证在同一时刻只有一个线程可以访问到共享资源,从而避免竞争条件的发生。 2. 分布式缓存:在分布式缓存中,多个节点可能同时访问同一个缓存数据。使用Lock接口可以确保在任意时刻只有一个节点可以对缓存数据进行修改或读取操作,从而保证数据的一致性和可靠性。 3. 分布式任务调度:在分布式任务调度中,多个节点可能同时竞争执行同一个任务。通过使用Lock接口,可以保证只有一个节点能够获得任务的执行权,从而避免重复执行或竞争问题的发生。 4. 分布式事务:在分布式事务中,多个节点可能同时操作同一个数据源。通过使用Lock接口,在进行事务提交或回滚时可以确保同一数据只能被一个节点访问,从而保证数据的一致性和完整性。 总之,Lock接口在Java分布式环境中可以用于解决并发访问问题,确保资源的互斥访问,并保证数据的一致性和可靠性。 ### 回答3: 在Java分布式环境中,Lock接口的使用场景主要是为了保证多个线程或者多个进程之间的数据操作的安全性和一致性。 首先,Lock接口可以用于保护共享资源的访问。在分布式系统中,多个处理节点可能同时访问同一个共享资源,而Lock接口可以提供互斥机制,确保同时只能有一个节点能够对资源进行操作。比如,在一个分布式数据库中,多个节点同时对同一条数据进行写操作,使用Lock接口可以保证在同一时间内只有一个节点能够成功写入,避免了数据的冲突。 其次,Lock接口也可以用于实现分布式的任务调度。在一个分布式系统中,多个节点可能需要按照一定的次序执行某些任务,而Lock接口提供了可重入的互斥机制,可以实现节点之间对任务的顺序控制。比如,一个分布式任务调度系统中,多个节点需要根据优先级依次执行任务,使用Lock接口可以确保按照优先级顺序对任务进行调度,避免了任务的乱序执行。 另外,Lock接口还可以用于实现分布式的事务处理。在分布式系统中,多个节点可能需要同时对多个资源进行操作,而Lock接口可以提供分布式事务中的锁机制,保证所有操作都能够成功完成或者回滚。比如,一个分布式订单处理系统中,多个节点需要同时对订单和库存进行操作,使用Lock接口可以保证在同一时间内只有一个节点能够对订单和库存进行修改,避免了订单和库存的不一致。 总之,Lock接口在Java分布式环境中的使用场景主要是为了保证数据操作的安全性和一致性,包括保护共享资源的访问、实现分布式的任务调度和实现分布式的事务处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值