Redis分布式锁介绍与使用

目录
  • 分布式锁
    • 业务逻辑分析
    • Redis命令
    • 代码实现
  • 分布式锁误删问题
    • 问题原因分析
    • 代码实现
  • Lua脚本

首先,使用idea模拟搭建一个tomcat服务器集群,并使用Nginx对集群中的服务器实现负载均衡

配置完负载均衡之后,发送两次请求就会在idea的运行窗口中发现,两次请求的运行是分别在两个服务器中完成,这就是集群的轮询机制

分布式锁

业务逻辑分析

  在单JVM虚拟机多线程执行的情况下,可以使用JVM内部的锁机制来控制多进程的并发执行,借此可以保证一个用户只能下一个优惠券订单。但是在分布式的情况下,每一个JVM虚拟机都有一个锁监视器,不同JVM里的不同线程之间的访问的并不是同一个锁监视器,所以说此时再使用synchronized锁就无法满足一个用户限买一单的业务情况了,于是就需要使用分布式锁

  分布式锁就是满足分布式系统或集群模式下多进程可见并且互斥的锁。一般实现分布式锁的技术主要就是MySQL、Redis和ZooKeeper,但是综合对比来看的话,Redis作分布式锁的性能更高一些,Redis是在JVM虚拟机之外的一种应用可以满足多线程都可见,互斥可以使用setnx这种的互斥命令来实现,但是使用Redis会存在安全性问题,如果Redis崩溃的话会导致锁无法释放而出现死锁现象,解决这一问题的方案就是使用TTL过期时间,就算崩溃也可以实现到期自动释放。

Redis命令

  使用Redis实现分布式锁的步骤主要就是使用setnx体现互斥锁,然后expire过期时间防止宕机死锁,但是如果服务在setnx之后expire之前宕机的话,依旧会造成死锁现象。于是我们可以使用以下命令在互斥的同时设置超时时间,这样的话即是在设置锁之后宕机,依旧可以凭借超时时间释放锁

SET lock thread NX EX ttl超时时间

代码实现

  将获取锁和释放锁业务抽取出来,使用接口和实现类来完成

public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec 锁的超时时间
     * @return 是否成功获取锁
     */
    boolean tryLock(long timeoutSec);
    /**
     * 释放锁
     */
    void unLock();
}
public class SimpleRedisLock implements ILock {
    private String name;
    /**
     *先获取StringRedisTemplate对象,才能使用代码操作Redis
     */
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前操作线程的标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean res = stringRedisTemplate.opsForValue()
                .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // res是Boolean的包装类,返回结果的时候涉及到拆箱问题,有可能存在结果为null的情况,此时就需要返回结果与true的比较,避免了空指针风险
        return Boolean.TRUE.equals(res);
    }
    @Override
    public void unLock() {
        // 释放锁
        stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);
    }
}

  定义了分布式锁的获取和释放,接下来就是在一人一单业务代码中将锁机制升级成多线程锁了,主要修改的代码为就是5~14行,由单体的synchronized锁改为使用自定义的Redis锁,并根据不同线程获取锁的不同结果定义了不同的业务

public Result secKillVoucher(Long voucherId) {
    // 单用户id(拦截器中做登录验证的用户id)
    Long userId = UserHolder.getUser().getId();
    // 创建锁对象
    SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
    // 获取锁
    boolean isLock = lock.tryLock(1200);
    // 判断是否获取锁成功
    if (!isLock) {
        // 获取锁失败,返回错误或者重试
        return Result.fail("不允许重复下单!" );
    }
    // 获取锁成功,继续下单的业务逻辑
    try {
        // 查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        // 获取时间 判断秒杀活动是否开始或者结束
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("活动暂未开始");
        } else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("活动已经结束");
        }
        // 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足,活动结束");
        }
        // user_id和voucher_id联合查询订单数
        int count = query().eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        // 订单数为1 就说明已经下过单了
        if (count > 0) {
            return Result.fail("您已经购买过该商品了");
        }
        // 扣减库存
        boolean update = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!update) {
            return Result.fail("库存不足!");
        }
        // 创建订单 并返回id
        VoucherOrder order = new VoucherOrder();
        // 订单id(redis全局唯一id) 下单用户id(拦截器中做登录验证的用户id) 优惠券id(直接传过来的id)
        long orderId = generator.nextId("order");
        order.setId(orderId);
        order.setUserId(userId);
        order.setVoucherId(voucherId);
        save(order);
        return Result.ok(orderId);
    } finally {
        // 释放锁
        lock.unLock();
    }
}

分布式锁误删问题

问题原因分析

  这个问题出现在Redis锁设置的超时时间上,由于设置了超时时间,所以可能出现一下情况:即当线程1获取到锁之后执行下单业务,但是由于业务堵塞锁已经超出TTL时间自动释放;此时线程2趁机获取Redis锁成功执行下单业务,线程2的下单业务执行到一半时线程1完成下单使用del命令释放锁;此时线程1释放的是线程2的锁,于是现在锁又处于闲置状态,于是线程3来获取Redis锁成功执行下单业务;此时,一共有同一个用户的两个线程在同时操作

  为了解决以上出现的问题,需要在每次释放锁之前都通过锁的线程标识(Redis锁对应的值)判断一下是不是自己的锁,如果是就使用del命令释放锁,否则就不做操作。但是有一点值得注意,之前锁的线程标识使用的是线程的name,这样的话很容易就造成不同JVM虚拟机里的线程name冲突影响判断,于是可以使用UUID随机生成一组数字加上线程name作为线程的标识,这样更能确保唯一性

代码实现

  综上所述,一共有两处需要改进的地方,一个是使用UUID加线程name作为线程标识(主要修改的是获取锁方法加上UUID的获取),一个是在使用del释放锁之前判断一下是否是自己的锁

public static final String ID_PREFIX = UUID.randomUUID(true) + "-";
public boolean tryLock(long timeoutSec) {
    // 获取当前操作线程的标识
    String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();
    // 获取锁
    Boolean res = stringRedisTemplate.opsForValue()
            .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    // res是Boolean的包装类,返回结果的时候涉及到拆箱问题,有可能存在结果为null的情况,此时就需要返回结果与true的比较,避免了空指针风险
    return Boolean.TRUE.equals(res);
}
public void unLock() {
    // 获取当前操作线程的标识
    String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();
    // 通过锁名 获取redis中存储的锁对应的标识
    String rid = stringRedisTemplate.opsForValue().get(RedisConstants.KEY_PREFIX + name);
    if (threadId.equals(rid)) {
        // 释放锁
        stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);
    }
}

Lua脚本

  Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:传送门

使用Redis命令调用脚本的常见命令可以是:

EVAL “redis.call(‘set’, ‘key’, ‘value’)” num

  上述命令解释为EVAL是调用,后面双引号中就是所调用的脚本语句,而最后的num即脚本语句中的KEYS类型参数的个数,num之外的就是ARGV(value)类型的参数。比如说,接下来这一个语句就代表着:setname为Rose,其中KEYS类型的参数有1个,就是num后面的第一个name,剩下的都是ARGV(value)类型的数据,其中调用的是KEYS[1]和ARGV[2],也就是name和Rose

EVAL “redis.call(‘set’, ‘KEYS[1]’, ‘ARGV[2]’)” 1 name age Rose

到此这篇关于Redis分布式锁介绍与使用的文章就介绍到这了,更多相关Redis分布式锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实现redis分布式锁的三种方式

    目录 一.引入原因 二.分布式锁实现过程中的问题 问题一:异常导致锁没有释放 问题二:获取锁与设置过期时间操作不是原子性的 问题三:锁过期之后被别的线程重新获取与释放 问题四:锁的释放不是原子性的 问题五:其他的问题? 三.具体实现 1. RedisTemplate 2. RedisLockRegistry 3. 使用redisson实现分布式锁 一.引入原因 在分布式服务中,常常有如定时任务.库存更新这样的场景. 在定时任务中,如果不使用quartz这样的分布式定时工具,只是简单的使用定时器来

  • Redisson如何解决Redis分布式锁提前释放问题

    目录 前言: 一.问题描述: 二.原因分析: 三.解决方案: 1.思考: 2.Redisson简单配置: 3.使用样例: 四.源码分析 1.lock加锁操作 2.unlock解锁操作 总结: 相关参考: 前言: 在分布式场景下,相信你或多或少需要使用分布式锁来访问临界资源,或者控制耗时操作的并发性. 当然,实现分布式锁的方案也比较多,比如数据库.redis.zk 等等.本文主要结合一个线上案例,讲解 redis 分布式锁的相关实现. 一.问题描述: 某天线上出现了数据重复处理问题,经排查后发现,

  • Spring Boot 集成Redisson实现分布式锁详细案例

    目录 前言 分布式锁实现 引入jar包 Redisson的配置 application.yml中引入redisson.yml配置 redisson.yml配置 封装Redisson工具类 模拟秒杀扣减库存 测试代码 总结 前言 Spring Boot集成Redis实现单机分布式锁针对单机分布式锁还是存在锁定续期.可重入的问题,本文将采用Spring Boot 集成Ression实现分布式锁进行详细讲解. 分布式锁实现 引入jar包 <dependency> <groupId>org

  • Redis分布式锁解决秒杀超卖问题

    目录 分布式锁应用场景 单体锁的分类 分布式锁核心逻辑 分布式锁实现的问题——死锁和解决 Redis解决删除别人锁的问题 分布式锁应用场景 秒杀环境下:订单服务从库存中心拿到库存数,如果库存总数大于0,则进行库存扣减,并创建订单订单服务负责创建订单库存服务负责扣减库存 模拟用户访问库存 多线程并发访问,出现超卖问题,线程不安全.没有保证原子性 单体锁的分类 单体应用锁指的是只能在 一个JVM 进程内有效的锁.我们把这种锁叫做单体应用锁 synchronized锁ReentrantLock锁一个

  • Redis常见分布锁的原理和实现

    目录 前言 基于数据库 悲观锁 实现原理 具体实现 乐观锁 简介 实现原理 具体实现 Redis实现分布式锁 Zooker实现分布式锁 加锁过程 释放锁的过程 异常场景分析 具体实现 Zookpeer实现分布式锁实现库存扣减 总结 前言 Java中的锁主要包括synchronized锁和JUC包中的锁,这些锁都是针对单个JVM实例上的锁,对于分布式环境是无效的,那么基于分布式锁的如何实现呢? 常见的分布式锁的实现如下图: 基于数据库 悲观锁 悲观锁(Pessimistic Lock)顾名思义为很

  • redis分布式锁的8大坑总结梳理

    目录 前言 1 非原子操作 2 忘了释放锁 3 释放了别人的锁 4 大量失败请求 5 锁重入问题 6 锁竞争问题 6.1 读写锁 6.2 锁分段 7 锁超时问题 8 主从复制的问题 前言 在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中. 但不是说用了redis分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引来一些意想不到的问题. 今天我们就一起聊聊redis分布式锁的一些坑,给有需要的朋友一个参考. 1 非原子操作 使用r

  • 详解Spring Cache使用Redisson分布式锁解决缓存击穿问题

    目录 1 什么是缓存击穿 2 为什么要使用分布式锁 3 什么是Redisson 4 Spring Boot集成Redisson 4.1 添加maven依赖 4.2 配置yml 4.3 配置RedissonConfig 5 使用Redisson的分布式锁解决缓存击穿 1 什么是缓存击穿 一份热点数据,它的访问量非常大.在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃. 2 为什么要使用分布式锁 在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制.在单机环境

  • Redis分布式锁介绍与使用

    目录 分布式锁 业务逻辑分析 Redis命令 代码实现 分布式锁误删问题 问题原因分析 代码实现 Lua脚本 首先,使用idea模拟搭建一个tomcat服务器集群,并使用Nginx对集群中的服务器实现负载均衡 配置完负载均衡之后,发送两次请求就会在idea的运行窗口中发现,两次请求的运行是分别在两个服务器中完成,这就是集群的轮询机制 分布式锁 业务逻辑分析   在单JVM虚拟机多线程执行的情况下,可以使用JVM内部的锁机制来控制多进程的并发执行,借此可以保证一个用户只能下一个优惠券订单.但是在分

  • Redis分布式锁详细介绍

    目录 分布式锁 redis实现分布式锁的原理 死锁问题 超时问题 锁误放问题 可重入性 Redlock 分布式锁 在单进程应用中,当一段代码同一时间内只能由一个线程执行时, 多线程下可能会出错,例如两个线程同时对一个数字做累加,两个线程同时拿到了该数字,例如40,一个线程加了10,一个线程加了20,正确结果应该是70, 但由于两个线程在自己的内存中一个算出的是50,一个算出的是60,此时二者都将自己的结果往该数字原本的地方写(保存), 这时候,肯定会有一个线程的值会被覆盖,因为读取->计算->

  • java语言描述Redis分布式锁的正确实现方式

    分布式锁一般有三种实现方式:1.数据库乐观锁:2.基于Redis的分布式锁:3.基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在持有锁的期间

  • 浅谈Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个

  • Java Redis分布式锁的正确实现方式详解

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在

  • springboot redis分布式锁代码实例

    这篇文章主要介绍了springboot redis分布式锁代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 随着微服务等分布式架构的快速发展及应用,在很多情况下,我们都会遇到在并发情况下多个线程竞争资源的情况,比如我们耳熟能详的秒杀活动,多平台多用户对同一个资源进行操作等场景等.分布式锁的实现方式有很多种,比如基于数据库.Zookeeper.Redis等,本文我们主要介绍Spring Boot整合Redis实现分布式锁. 工具类如下: i

  • Redis分布式锁的正确实现方法总结

    分布式锁一般有三种实现方式: 1.数据库乐观锁: 2.基于Redis的分布式锁: 3.基于ZooKeeper的分布式锁. 本文将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个客户端在

  • Redis分布式锁的实现方式(redis面试题)

    什么是分布式锁? 要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁.进程锁. 线程锁:主要用来给方法.代码块加锁.当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段.线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state). 进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronize

  • redis分布式锁及会出现的问题解决

    一.redis实现分布式锁的主要原理: 1.加锁 最简单的方法是使用setnx命令.key是锁的唯一标识,按业务来决定命名.比如想要给一种商品的秒杀活动加锁,可以给key命名为 "lock_sale_商品ID" .而value设置成什么呢?我们可以姑且设置成1.加锁的伪代码如下: setnx(key,1) 当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁:当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败. 2.解锁 有加锁就得有解锁.当得到锁的

  • Redis分布式锁的使用和实现原理详解

    模拟一个电商里面下单减库存的场景. 1.首先在redis里加入商品库存数量. 2.新建一个Spring Boot项目,在pom里面引入相关的依赖. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <gr

随机推荐