Spring Boot 实现Redis分布式锁原理

目录
  • 分布式锁实现
    • 引入jar包
    • 封装工具类
    • 模拟秒杀扣减库存
    • 测试代码
  • 方案优化
    • 问题1:扣减库存逻辑无法保证原子性,
    • 问题2:如果加锁失败,则会直接访问,无法重入锁
  • 总结

分布式锁实现

引入jar包

<dependency>
 <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
   <exclusion>
 <groupId>io.lettuce</groupId>
 <artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
 <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
 </dependency>

说明:本文采用jedis来实现分布式锁。

封装工具类

@Component
public class RedisLockUtil
{
    private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 加锁方法仅针对单实例 Redis,哨兵、集群模式无法使用
     *
     * @param lockKey 加锁键
     * @param clientId 加锁客户端唯一标识(采用UUID)
     * @param seconds 锁过期时间
     * @return true标识加锁成功、false代表加锁失败
     */
    public Boolean tryLock(String lockKey, String clientId, long seconds)
    {
        try
        {
            return redisTemplate
                    .execute((RedisCallback<Boolean>) redisConnection -> {
                        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                        SetParams params =new SetParams();
                        params.nx();
                        params.px(seconds);
                        String result = jedis.set(lockKey, clientId, params);
                        if (LOCK_SUCCESS.equals(result))
                        {
                            return Boolean.TRUE;
                        }
                        return Boolean.FALSE;
                    });
        }
        catch (Exception e)
        {
            logger.error("tryLock error",e);
        }

        return false;
    }
    /**
     *释放锁,保持原子性操作,采用了lua脚本
     *
     * @param lockKey
     * @param clientId
     * @return
     */
    public Boolean unLock(String lockKey, String clientId)
    {
        try
        {
            return  redisTemplate
                    .execute((RedisCallback<Boolean>) redisConnection -> {
                        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                        Object result = jedis.eval(RELEASE_LOCK_SCRIPT,
                                Collections.singletonList(lockKey),
                                Collections.singletonList(clientId));
                        if (RELEASE_SUCCESS.equals(result))
                        {
                            return Boolean.TRUE;
                        }
                        return Boolean.FALSE;
                    });
        }
        catch (Exception e)
        {
            logger.error("unlock error",e);
        }
        return Boolean.FALSE;
    }
}

说明:加锁的原理是基于Redis的NX、PX命令,而解锁采用的是lua脚本实现。

模拟秒杀扣减库存

public int lockStock()
    {
        String lockKey="lock:stock";
        String clientId = UUID.randomUUID().toString();
        long seconds =1000l;

        try
        {
            //加锁
            boolean flag=redisLockUtil.tryLock(lockKey, clientId, seconds);
            //加锁成功
            if(flag)
            {
               logger.info("加锁成功 clientId:{}",clientId);
               int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock"));
               if(stockNum>0)
               {
                  stockNum--;
                  redisUtil.set("seckill:goods:stock",String.valueOf(stockNum));
                  logger.info("秒杀成功,剩余库存:{}",stockNum);
               }
               else
               {
                  logger.error("秒杀失败,剩余库存:{}", stockNum);
               }
               //获取库存数量
               return stockNum;
            }
            else
            {
                logger.error("加锁失败:clientId:{}",clientId);
            }
        }
        catch (Exception e)
        {
           logger.error("decry stock eror",e);
        }
        finally
        {
           redisLockUtil.unLock(lockKey, clientId);
        }
        return 0;
    }

测试代码

@RequestMapping("/redisLockTest")
    public void redisLockTest()
    {
        // 初始化秒杀库存数量
        redisUtil.set("seckill:goods:stock", "10");

        List<Future> futureList = new ArrayList<>();

        //多线程异步执行
        ExecutorService executors = Executors.newScheduledThreadPool(10);
        //
        for (int i = 0; i < 30; i++)
        {
            futureList.add(executors.submit(this::lockStock));

            try
            {
               Thread.sleep(100);
            }
            catch (InterruptedException e)
            {
               logger.error("redisLockTest error",e);
            }
        }

        // 等待结果,防止主线程退出
        futureList.forEach(t -> {
            try
            {
                int stockNum =(int) t.get();
                logger.info("库存剩余数量:{}",stockNum);
            }
            catch (Exception e)
            {
               logger.error("get stock num error",e);
            }
        });
    }

执行结果如下:

方案优化

上述分布式锁实现库存扣减是否存在相关问题呢?

问题1:扣减库存逻辑无法保证原子性,

具体的代码如下:

int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock"));
if(stockNum>0)
 {
    stockNum--;
    redisUtil.set("seckill:goods:stock",String.valueOf(stockNum));
 }

这是典型的RMW模型,前面章节已经介绍了具体的实现方案,可以采用lua脚本和Redis的incry原子性命令实现,这里采用lua脚本来实现原子性的库存扣减。

具体实现如下:

  public long surplusStock(String key ,int num)
   {
       StringBuilder lua_surplusStock = new StringBuilder();
       lua_surplusStock.append("   local key = KEYS[1];");
       lua_surplusStock.append("   local subNum = tonumber(ARGV[1]);");
       lua_surplusStock.append("   local surplusStock=tonumber(redis.call('get',key));");
       lua_surplusStock.append("    if (surplusStock- subNum>= -1) then");
       lua_surplusStock.append("        return redis.call('incrby', KEYS[1], 0-subNum);");
       lua_surplusStock.append("    else ");
       lua_surplusStock.append("    return -1;");
       lua_surplusStock.append("    end");

       List<String> keys = new ArrayList<>();
       keys.add(key);
       // 脚本里的ARGV参数
       List<String> args = new ArrayList<>();
       args.add(Integer.toString(num));

       long result = redisTemplate.execute(new RedisCallback<Long>() {
           @Override
           public Long doInRedis(RedisConnection connection) throws DataAccessException {
               Object nativeConnection = connection.getNativeConnection();
               // 单机模式
               if (nativeConnection instanceof Jedis)
               {
                   return (Long) ((Jedis) nativeConnection).eval(lua_surplusStock.toString(), keys, args);
               }
               return -1l;
           }
       });
       return result;
   }

问题2:如果加锁失败,则会直接访问,无法重入锁

因为单机版本的锁是无法重入锁,所以加锁失败就直接返回,此问题的解决方案,可以采用Redisson来实现,关于Redisson实现分布式锁,将在后续的文章中进行详细的讲解。

总结

本文主要讲解了Spring Boot集成Redis实现单机版本分布式锁,虽然单机版分布式锁存在锁的续期、锁的重入问题,但是我们还是需要掌握其原理和实现方法

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

(0)

相关推荐

  • 关于SpringBoot 使用 Redis 分布式锁解决并发问题

    目录 问题背景 解决方案 主要实现原理: 可靠性: SpringBoot 集成使用 Redis 分布式锁 使用示例 参考文档 问题背景 现在的应用程序架构中,很多服务都是多副本运行,从而保证服务的稳定性.一个服务实例挂了,其他服务依旧可以接收请求.但是服务的多副本运行随之也会引来一些分布式问题,比如某个接口的处理逻辑是这样的:接收到请求后,先查询 DB 看是否有相关的数据,如果没有则插入数据,如果有则更新数据.在这种场景下如果相同的 N 个请求并发发到后端服务实例,就会出现重复插入数据的情况:

  • SpringBoot之使用Redis实现分布式锁(秒杀系统)

    一.Redis分布式锁概念篇 建议直接采用Redis的官方推荐的Redisson作为redis的分布式锁 1.1.为什么要使用分布式锁 我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug! 注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间! 后来业务发展,需要做集群,一个应用需要部署到几台机

  • SpringBoot中使用redis做分布式锁的方法

    一.模拟问题 最近在公司遇到一个问题,挂号系统是做的集群,比如启动了两个相同的服务,病人挂号的时候可能会出现同号的情况,比如两个病人挂出来的号都是上午2号.这就出现了问题,由于是集群部署的,所以单纯在代码中的方法中加锁是不能解决这种情况的.下面我将模拟这种情况,用redis做分布式锁来解决这个问题. 1.新建挂号明细表 2.在idea上新建项目 下图是创建好的项目结构,上面那个parent项目是其他项目不用管它,和新建的没有关系 3.开始创建controller,service,dao(mapp

  • SpringBoot RedisTemplate分布式锁的项目实战

    目录 1.使用场景 2.加锁解决 3.分布式锁 4.增加失效时间 5.增加线程唯一值 6.Lua脚本 7.Lua是如何实现原子性的 8.代码演示 9. 总结 1.使用场景 想直接获取加锁和解锁代码,请直接到代码处 在下单场景减库存时我们一般会将库存查询出来,进行库存的扣除 @GetMapping(value = "order") public R order() { int stock = RedisUtil.getObject("stock", Integer.c

  • Springboot整合Redis实现超卖问题还原和流程分析(分布式锁)

    目录 超卖简单代码 超卖问题 单服务器单应用情况下 设置synchronized Redis实现分布式锁 通过超时间解决上述问题 通过key设置值匹配的方式解决形同虚设问题 最终版 超卖简单代码 写一段简单正常的超卖逻辑代码,多个用户同时操作同一段数据,探究出现的问题. Redis中存储一项数据信息,请求对应接口,获取商品数量信息: 商品数量信息如果大于0,则扣减1,重新存储Redis中: 运行代码测试问题. /** * Redis数据库操作,超卖问题模拟 * @author * */ @Res

  • SpringBoot使用Redisson实现分布式锁(秒杀系统)

    前面讲完了Redis的分布式锁的实现,接下来讲Redisson的分布式锁的实现,一般提及到Redis的分布式锁我们更多的使用的是Redisson的分布式锁,Redis的官方也是建议我们这样去做的.Redisson点我可以直接跳转到Redisson的官方文档. 1.1.引入Maven依赖 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter&l

  • springboot 集成redission 以及分布式锁的使用详解

    目录 springboot集成redission及分布式锁的使用 1.引入jar包 2.增加Configuration类 3.使用redission分布式锁 Springboot整合Redisson 锁 一.依赖 二.配置文件 三.锁的使用 四.分布式秒杀 五.redis锁 单机版可用,分布式用Redisson springboot集成redission及分布式锁的使用 1.引入jar包 <dependency> <groupId>org.redisson</groupId&

  • Spring boot 整合 Redisson实现分布式锁并验证功能

    目录 简述 1. 在idea中新建spring boot工程并引入所需依赖 2. 编写相关代码实现 3. 模拟实际环境验证 3.1 下载idea的docker插件并配置相关镜像信息 3.2 将spring boot打包的jar构建为docker镜像 3.2 配置nginx 3.3 下载安装Jmeter进行测试 简述 整篇文章写的比较粗糙,大佬看了轻喷.前半部分 是整合spring boot和redisson, 后半部分是验证分布式锁.在整个过程中遇见了不少的问题,在此做个记录少走弯路 redis

  • SpringBoot整合Redisson实现分布式锁

    目录 一.添加依赖 二.redis配置文件 三.新建配置类 四.使用分布式锁 可重入锁 读写锁 信号量(Semaphore) 闭锁(CountDownLatch) Redisson是架设在redis基础上的一个Java驻内存数据网格(In-Memory Data Grid).充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类.使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低

  • Spring Boot 实现Redis分布式锁原理

    目录 分布式锁实现 引入jar包 封装工具类 模拟秒杀扣减库存 测试代码 方案优化 问题1:扣减库存逻辑无法保证原子性, 问题2:如果加锁失败,则会直接访问,无法重入锁 总结 分布式锁实现 引入jar包 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclus

  • 利用consul在spring boot中实现分布式锁场景分析

    因为在项目实际过程中所采用的是微服务架构,考虑到承载量基本每个相同业务的服务都是多节点部署,所以针对某些资源的访问就不得不用到用到分布式锁了. 这里列举一个最简单的场景,假如有一个智能售货机,由于机器本身的原因不能同一台机器不能同时出两个商品,这就要求在在出货流程前针对同一台机器在同一时刻出现并发 创建订单时只能有一笔订单创建成功,但是订单服务是多节点部署的,所以就不得不用到分布式锁了. 以上只是一种简单的业务场景,在各种大型互联网实际应用中,需要分布式锁的业务场景会更多,综合比较了业界基于各种

  • redis分布式锁的实现原理详解

    首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁. 3.具有容错性.只要大部分的Redis节点正常运行,客户端就可以加锁和解锁. 4.解铃还须系铃人.加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了. 下边是代码实现,首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码: <depe

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

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

  • spring boot整合redis实现shiro的分布式session共享的方法

    我们知道,shiro是通过SessionManager来管理Session的,而对于Session的操作则是通过SessionDao来实现的,默认的情况下,shiro实现了两种SessionDao,分别为CachingSessionDAO和MemorySessionDAO,当我们使用EhCache缓存时,则是使用的CachingSessionDAO,不适用缓存的情况下,就会选择基于内存的SessionDao.所以,如果我们想实现基于Redis的分布式Session共享,重点在于重写Session

  • 详解Redis分布式锁的原理与实现

    目录 前言 使用场景 为什么要使用分布式锁 如何使用分布式锁 流程图 分布式锁的状态 分布式锁的特点 分布式锁的实现方式(以redis分布式锁实现为例) 总结 前言 在单体应用中,如果我们对共享数据不进行加锁操作,会出现数据一致性问题,我们的解决办法通常是加锁.在分布式架构中,我们同样会遇到数据共享操作问题,此时,我们就需要分布式锁来解决问题,下面我们一起聊聊使用redis来实现分布式锁. 使用场景 库存超卖 比如 5个笔记本 A 看 准备买3个 B 买2个 C 4个 一下单 3+2+4 =9

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

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

  • springboot redis分布式锁代码实例

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

  • 详解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分布式锁Redisson及SpringBoot集成Redisson

    目录 - 分布式锁需要具备的条件和刚需 - Redisson使用 - SpringBoot集成Redisson - 分布式锁需要具备的条件和刚需 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这

随机推荐