Redis实现分布式锁

523 阅读6分钟

什么是分布式锁

分布式锁是一种应用级的锁,在分布式系统下一个应用通常会有多个节点部署,那么在高并发的情况下,就可能出现同一时间多个节点多个线程同时处理同一条数据,这种情况很容易出现数据不一致。那么分布式锁的出现就是为了解决此问题,它能够保证一个代码块在同一时间只能被同一线程或同一个节点执行。

分布式锁有哪些

  • 基于数据库:使用数据库设计排他锁或者共享锁来实现。
  • 基于缓存:由于缓存操作是原子性的,可以使用Redis或Memcached进行实现分布式锁。
  • 基于Zookeeper:利用其临时顺序节点特性实现。
  • 基于分布式协调服务:使用Etcd或Consul的API实现。

没种分布式锁的实现原理也有些不同,今天我们主要基于Redis缓存进一步说明分布式锁的实现。

适用场景

  • 数据并发读写:当多个节点需要对同一数据进行操作时,分布式锁可以确保在任何时刻只有一个节点能够执行写操作,从而避免数据的不一致。
  • 业务流程控制:在复杂的业务流程中,多个节点可能需要按特定顺序执行某些操作。可以使用分布式锁来保证这些流程按顺序执行。
  • 资源争抢:当多个节点需要争抢有限的资源时,分布式锁可以避免资源竞争导致的系统压力增大或资源浪费。

实现原理

使用Redis实现分布式锁主要分为以下几个步骤: 1.使用RedissetNx方法,设置key-value到缓存中,由于Redis操作是原子性的,并且该方法操作若key在缓存中不存在,则会创建key并设值后返回状态码1,否则返回状态码0,所以我们只需根据返回的状态码就可以确定是否有线程正在占用资源,以此作为是否加锁成功的依据。 2.使用Redisdelete方法,可以在流程执行完成后对缓存进行删除,以此达到释放锁的目的,从而使其他线程可以加锁进行操作。 3.使用Redisexpire方法,设置缓存过期时间,主要用于防止一些故障导致缓存没有释放,而长时间占用锁从而导致死锁,同时也可以为长时间的任务进行续期。

分布式锁设计实现

这里我们主要通过SpringBoot已经封装好的RedisTemplate工具类进行详细讲解。

  • 引入maven依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
  <version>2.2.5.RELEASE</version>
</dependency>
  • 话不多说直接上代码
public class RedisLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    /**
     * 锁状态
     */
    private Boolean locked = false;

    /**
     * Redis操作工具
     */
    private RedisTemplate redisTemplate;

    /**
     * 缓存过期时间(单位:秒)
     */
    private Long expireTime = 30L;

    /**
     * 缓存key
     */
    private String key;

    public RedisLock(RedisTemplate redisTemplate, String key) {
        this.redisTemplate = redisTemplate;
        this.key = key;
    }

    public RedisLock(RedisTemplate redisTemplate, String key, Long expireTime) {
        this(redisTemplate, key);
        this.expireTime = expireTime;
    }

    public boolean tryLock(long intervalTimeMs, long timeoutMs) {
        // 重试间隔时间,默认100
        if (intervalTimeMs <= 0) {
            intervalTimeMs = 100L;
        }
        // 获取锁超时时间,默认100
        if (timeoutMs <= intervalTimeMs) {
            timeoutMs = 100L;
        }
        while (timeoutMs >= 0) {
            // 设置缓存,底层调用setNx方法,返回true即缓存设置成功,则表示加锁成功
            if (redisTemplate.opsForValue().setIfAbsent(key, "1", expireTime, TimeUnit.SECONDS)) {
                locked = true;
                return true;
            }
            // 否则按间隔时间进行休眠后再重新尝试加锁
            sleep(intervalTimeMs);
            timeoutMs -= intervalTimeMs;
        }
        locked = false;
        return false;
    }

    public void releaseLock() {
        if (!locked) {
            return;
        }
        // 仅已加锁才进行删除
        redisTemplate.delete(key);
        locked = false;
    }

    /**
     * 延长过期时间
     */
    public void renewal() {
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    }

    /**
     * 自定义延长过期时间
     * @param renewalTime 延长的时间(单位:秒)
     */
    public void renewal(long renewalTime) {
        redisTemplate.expire(key, renewalTime, TimeUnit.SECONDS);
    }

    private void sleep(long intervalTimeMs) {
        try {
            TimeUnit.MILLISECONDS.sleep(intervalTimeMs);
        } catch (InterruptedException e) {
            logger.error("tryLock:{} timeout exception", key);
        }
    }
}

public class RedisLockBuild {
    public static RedisLock build(RedisTemplate redisTemplate, String key) {
        RedisLock redisLock = new RedisLock(redisTemplate, key);
        return redisLock;
    }

    public static RedisLock build(RedisTemplate redisTemplate, String key, long expireTime) {
        RedisLock redisLock = new RedisLock(redisTemplate, key, expireTime);
        return redisLock;
    }
}

从以上的两个代码类来看是不是觉得如此简单,主要流程点都有注释,接下来我们就仔细说明一下这两个类的作用:

RedisLock: Redis分布式锁工具类,主要提供以下几个方法:

  • public boolean tryLock(long intervalTimeMs, long timeoutMs)
    • 尝试获取锁,并传递参数作为获取不到锁时进行等待,以及等待每次重新获取锁的时间,在有效时间内获取到锁就返回成功,否则返回失败,业务侧根据获取锁的结果进行业务处理。
  • public void releaseLock()
    • 释放锁,通常为了在锁使用完成后对其进行快速释放,供其他线程进行使用,所以在加锁的代码块都需要使用try-finally进行处理,必须主动在finally处进行锁释放。
  • public void renewal(long renewalTime)
    • 为防止有个别处理时间较长的流程,防止流程未处理完锁就过期,提供此方法用于对锁进行续期,也就是对锁重新设置过期时间,以达到延迟过期的效果,使用需要注意释放锁防止造成死锁。

RedisLockBuild: 作为RedisLock的构造器,主要方便我们构造RedisLock对象,有需要的可根据个人所需进行封装。

使用案例

看似简单的东西,不动手试一下怎么知道好不好用呢,这里我们就举个栗子来说明一下:

@RestController
@RequestMapping("/redis")
public class RedisController {

    private static final Logger logger = LoggerFactory.getLogger(RedisController.class);

    @Autowired
    private RedisTemplate redisTemplate;

    private Integer inventoryNumber = 10;

    @PostMapping("/lock")
    public String lock() {
        CompletableFuture cf1 = CompletableFuture.runAsync(() -> increaseInventory(1));
        CompletableFuture cf2 = CompletableFuture.runAsync(() -> decreaseInventory(1));
        CompletableFuture<Void> cfAll = CompletableFuture.allOf(cf1, cf2);
        cfAll.join();
        logger.info("Inventory Number:{}", inventoryNumber);
        return "test success";
    }

    /**
     * 增加库存
     */
    @SneakyThrows
    public void increaseInventory(int number) {
        logger.info("----- increaseInventory start -----");
        RedisLock redisLock = RedisLockBuild.build(redisTemplate, "inventory");
        try {
            if (redisLock.tryLock(500, 3000)) {
                inventoryNumber += number;
                logger.info("----- increaseInventory success sleep 2 seconds -----");
                TimeUnit.SECONDS.sleep(2);
            }
            else {
                logger.info("increaseInventory lock failed.");
            }
        }
        finally {
            redisLock.releaseLock();
        }
        logger.info("----- increaseInventory end -----");
    }

    /**
     * 减少库存
     */
    @SneakyThrows
    public void decreaseInventory(int number) {
        logger.info("----- decreaseInventory start -----");
        RedisLock redisLock = RedisLockBuild.build(redisTemplate, "inventory");
        try {
            if (redisLock.tryLock(500, 3000)) {
                inventoryNumber -= number;
                logger.info("----- decreaseInventory success sleep 2 seconds -----");
                TimeUnit.SECONDS.sleep(2);
            }
            else {
                logger.info("decreaseInventory lock failed.");
            }
        }
        finally {
            redisLock.releaseLock();
        }
        logger.info("----- decreaseInventory end -----");
    }
}

以上代码我们简单写了个测试类,设置一个库存变量inventoryNumber=10,两个任务cf1、cf2,并进行异步并发执行,这两个任务同时对库存变量inventoryNumber进行增加和递减,此时我们细看increaseInventory、decreaseInventory这两个方法里面的实现,同时都对库存修改的动作进行了加分布式锁操作,方式并发修改数据出现数据不一致问题。如此我们就成功实现了分布式锁。

image.png 为了更加可观,我们在代码里设置了休眠2秒的时间,上图是我们的一次运行结果,也就是无论哪个任务先获取到锁,另一个任务就要等休眠2秒过后释放后才能够获取锁,从而进行库存操作。

总结

使用分布式锁时也是需要合理分析场景,并合理设置好锁的时间,以达到锁资源的合理分配,从而使我们的系统在高效运行的情况下更加安全可靠。