Redis实现分布式锁机制

64 阅读4分钟

一、锁机制

在Java中,锁机制是用于多线程编程中实现线程同步的一种机制。它可以确保在同一时间只有一个线程可以访问共享资源,从而避免并发访问导致的数据不一致或竞态条件的问题。Java中的锁机制可以通过以下两种方式实现:

  1. Synchronized关键字:Synchronized关键字可以应用于方法或代码块。当一个线程获得了某个对象的锁后,其他线程将被阻塞,直到该线程释放锁。Synchronized关键字确保了同一时间只有一个线程可以执行被锁保护的代码块。
  2. ReentrantLock类:ReentrantLock是Java中的一个可重入锁实现,它提供了更灵活的锁定机制。与Synchronized关键字不同,ReentrantLock使用代码显式地获取和释放锁。使用ReentrantLock,可以更精确地控制锁的获取和释放过程,并提供更多高级功能,如公平性、条件变量等。

二、分布式锁

2.1 案例引入

对于简单的单体项目,即运行时程序在同一个Java虚拟机中,使用Java的锁机制(synchronized或者ReentrantLock)可以解决多线程并发问题。

但是在分布式环境中,程序是集群方式部署,如下图:

image.png

这时候可以通过启动两个服务实例来测试集群部署时线程并发问题,可以发现如果还是继续使用java的锁机制,会产生并发问题,造成数据的不一致现象。这是因为synchronized、ReentrantLock只是jvm级别的加锁,没有办法控制其他jvm。也就是上面两个tomcat实例还是可以出现并发执行的情况。要解决分布式环境下的并发问题,则必须使用分布式锁。

2.2 分布式锁介绍

分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保证数据的一致性。

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

实现分布式锁的方式很多,例如:Redis、数据库、Zookeeper等。

2.3 分布式锁的解决方案

2.3.1 SETNX

这种加锁的思路是,如果 key 不存在则为 key 设置 value,如果 key 已存在则 SETNX 命令不做任何操作

  • 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功 mykey->myvalue
  • 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败 mykey->myvalue
  • 客户端A执行代码完成,删除锁
  • 客户端B在等待一段时间后再去请求设置key的值,设置成功 mykey->myvalue
  • 客户端B执行代码完成,删除锁

格式

#尝试获取锁
127.0.0.1:6379> SETNX key value
#设置锁过期时间
127.0.0.1:6379> EXPIRE key seconds
#删除锁
127.0.0.1:6379> DEL key

为什么要设置key过期时间呢?

如果某个客户端获得锁后因为某些原因意外退出了,导致创建了锁但是没有来得及删除锁,那么这个锁将一直存在,后面所有的客户端都无法再获得锁,所以必须要设置过期时间。

2.3.2 SET

通过前面的expire命令来设置锁过期时间还存在一个问题,就是SETNX和EXPIRE两个命令不是原子性操作。在极端情况下可能会出现获取锁后还没来得及设置过期时间程序就挂掉了,这样就又出现了锁一直存在,后面所有的客户端都无法再获得锁的问题。

如何解决这个问题?答案是使用SET命令。

SET 命令从Redis 2.6.12 版本开始包含设置过期时间的功能,这样获取锁和设置过期时间就是一个原子操作了。

格式

SET key value [EX seconds] [NX]

示例

127.0.0.1:6379> SET mykey myvalue EX 5 NX
  • EX seconds :将键的过期时间设置为 seconds 秒
  • NX :只在key不存在时才对键进行设置操作

2.3.3 代码实现

 @Autowired
    RedisLock redisLock;

    //使用redis分布式锁,无阻塞
    @GetMapping("/stock3")
    public String stock3() {
        String mylock = redisLock.tryLock("MYLOCK", 2000);
        if(mylock!=null){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                //放回redis
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("减库存成功,剩余库存是:" + stock);
            } else {
                System.out.println("库存不足了!");
            }

            redisLock.unlock("MYLOCK",mylock);
        }
        return "OK";
    }

    //使用redis分布式锁,有阻塞
    @GetMapping("/stock4")
    public String stock4() {
        String mylock = redisLock.lock("MYLOCK", 2000,1000);
        if(mylock!=null){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                //放回redis
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("减库存成功,剩余库存是:" + stock);
            } else {
                System.out.println("库存不足了!");
            }

            redisLock.unlock("MYLOCK",mylock);
        }
        return "OK";
    }