Redis实现分布式锁的示例代码

 更新时间:2023年10月25日 09:24:52   作者:生命猿于运动  
日常开发中,秒杀下单、抢红包等等业务场景,都需要用到分布式锁,本文主要介绍了Redis实现分布式锁的示例代码,感兴趣的可以了解一下

什么是分布式锁

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

分布式锁有哪些

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

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

适用场景

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

实现原理

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

分布式锁设计实现

这里我们主要通过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这两个方法里面的实现,同时都对库存修改的动作进行了加分布式锁操作,方式并发修改数据出现数据不一致问题。如此我们就成功实现了分布式锁。

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

总结

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

到此这篇关于Redis实现分布式锁的示例代码的文章就介绍到这了,更多相关Redis 分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  •  Redis 串行生成顺序编码的方法实现

     Redis 串行生成顺序编码的方法实现

    本文主要介绍了 Redis 串行生成顺序编码的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • Redis缓存空间优化实践详解

    Redis缓存空间优化实践详解

    缓存Redis,是我们最常用的服务,其适用场景广泛,被大量应用到各业务场景中。也正因如此,缓存成为了重要的硬件成本来源,我们有必要从空间上做一些优化,降低成本的同时也会提高性能,本文通过代码示例介绍了redis如何优化缓存空间,需要的朋友可以参考一下
    2023-04-04
  • CentOS系统下Redis安装和自启动配置的步骤

    CentOS系统下Redis安装和自启动配置的步骤

    相信大家都知道Redis是一个C实现的基于内存、可持久化的键值对数据库,在分布式服务中常作为缓存服务。所以这篇文章将详细介绍在CentOS系统下如何从零开始安装到配置启动服务。有需要的可以参考借鉴。
    2016-09-09
  • Redis String 类型和 Hash 类型学习笔记与总结

    Redis String 类型和 Hash 类型学习笔记与总结

    这篇文章主要介绍了Redis String 类型和 Hash 类型学习笔记与总结,本文分别对String 类型的一些方法和Hash 类型做了详细介绍,需要的朋友可以参考下
    2015-06-06
  • redis常用命令小结

    redis常用命令小结

    这篇文章主要介绍了redis的一些常用命令,需要的朋友可以参考下
    2014-06-06
  • Redis 如何清空所有数据

    Redis 如何清空所有数据

    这篇文章主要介绍了Redis 如何清空所有数据,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • Redis分布式锁实例分析讲解

    Redis分布式锁实例分析讲解

    分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性
    2022-12-12
  • Caffeine实现类似redis的动态过期时间设置示例

    Caffeine实现类似redis的动态过期时间设置示例

    这篇文章主要为大家介绍了Caffeine实现类似redis的动态过期时间示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • 浅谈一下Redis的数据结构

    浅谈一下Redis的数据结构

    这篇文章主要介绍了浅谈一下Redis的数据结构,简单字符串结构被用于存储redis的key对象和String类型的value对象,其中的free和len字段可以轻松的使得在该字符串被修改时判断是否需要扩容,需要的朋友可以参考下
    2023-08-08
  • Redis缓冲区溢出及解决方案分享

    Redis缓冲区溢出及解决方案分享

    Redis缓冲区溢出是指Redis缓冲区被写入的数据超过了它的容量,导致数据无法存储或被覆盖。造成缓冲区溢出的原因可能是快速写入大量数据、缓冲区未及时刷新或Redis服务器配置不当等。
    2023-04-04

最新评论