redis+lua脚本实现秒杀扣减库存 & SpringBoot环境+Redisson客户端

2023-05-16

redis+lua脚本实现秒杀扣减库存 & SpringBoot环境+Redisson客户端

  • 前言
  • 为什么使用Redisson
  • 项目搭建
    • maven配置
    • 编写Redisson配置类
    • 编写Application.yml
    • 编写启动类
    • 编写测试类测试Redisson是否连接成功
  • 使用lua脚本实现扣减库存
      • 代码实现

redis版本需要大于2.6

前言

秒杀场景为了防止库存超卖有很多种方式,数据库锁(主要用行锁)、分布式锁(redis或zk)、redis+lua脚本实现原子性操作等,使用数据库锁方案适合一些访问量不大的程序,使用分布式锁性能上可能会有所提升主要看业务处理方案,如果锁粒度比较大处理业务逻辑过多性能也不太高甚至比只用数据库锁性能更差,redis+lua脚本能比较好的解决超卖问题,同样的也需要自己处理好业务逻辑,这里只对redis+lua脚本怎么实现扣减库存做编写,业务上怎么处理自己发挥即可。

为什么使用Redisson

redisson是一个redis的客户端,它拥有丰富的功能,而且支持redis的各种模式,什么单机,集群,哨兵的都支持,各种各样的分布式锁实现,什么分布式重入锁,红锁,读写锁,然后它操作redis的常用数据结构就跟操作jdk的各种集合一样简单,底层实现中也是使用了大量的lua脚本操作redis。

项目搭建

maven配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
    </parent>

	<properties>
        <java.version>1.8</java.version>
        <lombok.version>1.18.12</lombok.version>
        <redisson.version>3.14.1</redisson.version>
    </properties>
	
	<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>${redisson.version}</version>
        </dependency>
    </dependencies>

编写Redisson配置类

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 单机连接
 */
@Data
@Configuration
public class RedissonConfig {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private Integer database;
    @Value("${spring.redis.connectTimeout}")
    private Integer connectTimeout;
    @Value("${spring.redis.timeout}")
    private Integer timeout;
    @Value("${spring.redis.connectionPoolSize}")
    private Integer connectionPoolSize;
    @Value("${spring.redis.connectionMinimumIdleSize}")
    private Integer connectionMinimumIdleSize;
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        String address = new StringBuilder("redis://").append(host).append(":").append(port).toString();
        SingleServerConfig singleServerConfig = config.useSingleServer();//使用单机redis
        singleServerConfig.setAddress(address);
        if (null != password && !"".equals(password.trim())) {
            singleServerConfig.setPassword(password);
        }
        singleServerConfig.setDatabase(database);
        singleServerConfig.setConnectTimeout(connectTimeout); //连接超时时间
        singleServerConfig.setTimeout(timeout); //命令执行等待时间
        singleServerConfig.setConnectionPoolSize(connectionPoolSize); //连接池
        singleServerConfig.setConnectionMinimumIdleSize(connectionMinimumIdleSize); //最小空闲连接数
        //设置String序列化
        //这里如果使用JsonJacksonCodec编码在使用hash结构时对应hash中的key会被套上一个双引号使用redis命令 (HGET key sonKey)这样是获取不到值的,加上双引号也不行
        config.setCodec(new StringCodec());
        return Redisson.create(config);
    }
}

编写Application.yml

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 1
    connectTimeout: 3000
    timeout: 3000
    connectionPoolSize: 50
    connectionMinimumIdleSize: 50

编写启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedissonApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedissonApplication.class);

    }
}

编写测试类测试Redisson是否连接成功

import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedissonTest {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 插入hash
     **/
    @Test
    public void t1() {
        //先存储hash结构的库存数据
        RMap<Object, Object> skuStock = redissonClient.getMap("stockInfo");
        skuStock.put("residueStock", 10000);
        skuStock.put("quantitySold", 0);
        System.out.println("插入库存信息成功 ---  ");
    }
}

使用lua脚本实现扣减库存

代码实现

import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.api.RMap;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀lua测试
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class SeckillLuaTest {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 模拟下单操作
     **/
    @Test
    public void t1() throws InterruptedException {
        long l = System.currentTimeMillis();

        //先存储hash结构的库存数据
        RMap<Object, Object> skuStock = redissonClient.getMap("stock");
        skuStock.put("residueStock",10000); //剩余库存
        skuStock.put("quantitySold",0); //已售数量
        System.out.println("插入10000个商品数量完成 ---  ");

        //创建最大核心线程数为1000的线程池,最多同时接收10000个任务
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1000, 1000, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(9000));
        //任务计数
        CountDownLatch countDownLatch = new CountDownLatch(10000);

        Random random = new Random();
        for (int i=0;i<10000;i++){
            threadPoolExecutor.execute(()->{
                //购买数量
                int num = random.nextInt(5);
                //扣除接口 0:库存不足下单失败  1:库存足够
                int result = Integer.valueOf(String.valueOf(deductInventory(num)));
                if(1==result){
                    System.out.println("下单成功,购买"+num+"个商品!");
                }else{
                    System.out.println("下单失败,购买"+num+"个商品但是库存不足!");
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        threadPoolExecutor.shutdown();

        System.out.println("-----------");

        System.out.println("模拟10000次下单耗时"+(System.currentTimeMillis()-l));
    }

    /*
    扣减库存lua
     */
    private Object deductInventory(int num){
		// lua脚本执行对象
        RScript script = redissonClient.getScript();
        
        // 编辑lua脚本
        StringBuilder temp = new StringBuilder();
        
        // 获取库存hash中stock -> residueStock 获取剩余库存
        // KEYS[1]相当于取keys中下标0 KEYS[2]相当于取keys中下标1
        // redis命令: HGET stock
        temp.append("local counter = redis.call('hget',KEYS[1],KEYS[2]);\n");
        
        // 当前库存减去需要购买的库存
        // ARGV[1]相当于传入的第一个num
        temp.append("local result  = counter - ARGV[1];\n");
        
        // 判断减去后是否大于等于0
        temp.append("if(result>=0 ) then\n");
        
        // 如果库存足够 将扣减后库存set到对应hash中并且使用hincrby给已售数量进行对应的增加操作
        temp.append("redis.call('hset',KEYS[1],KEYS[2],result);\n");
        temp.append("redis.call('hincrby',KEYS[1],KEYS[3],ARGV[1]);\n");
        
        // 库存信息处理成功后返回1
        temp.append("return 1;\n");
        temp.append("end;\n");
        
        // 库存不足返回0
        temp.append("return 0;\n");

		// 构建keys信息,代表hash值中所需要的key信息
        List<Object> keys = Arrays.asList("stock", "residueStock", "quantitySold");
        
        // 执行脚本
        Object result = script.eval(RScript.Mode.READ_WRITE, temp.toString(), RScript.ReturnType.VALUE,keys ,num);
        System.out.println("执行lua脚本后返回参数 "+result);
        return result;
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

redis+lua脚本实现秒杀扣减库存 & SpringBoot环境+Redisson客户端 的相关文章

随机推荐

  • 解决新安装的 Arch Linux 在 GNOME 中搜狗输入法不显示候选词和输入延迟的问题

    问题描述 新安装的 Arch Linux 系统 在 AUR 中安装 fcitx sogoupinyin 后 fcitx 的图标不显示 xff0c 且切换到搜狗输入法后不显示候选词 xff0c 输入的延迟也极高 xff0c 切换回英文输入法后
  • hashCode和equals作用

    什么是hashCode hashCode 的作用是获取哈希码 xff0c 返回一个int整数 xff0c 作用是查找hashMap的索引位置 hashCode 在JDK的Object类中 xff0c 也就表明每一个类中都有此方法 hashC
  • python学习——while True的用法

    古月金真 while True 是python中经常会被应用到 下面通过个例子进行解释 xff1a 例子 xff1a 建立一个用户登录系统 xff0c 用户输入用户名和密码 xff0c 如果正确就可以进入系统 法一 xff1a d 61 古
  • linux下将gbk文件转换为utf-8

    titledatetagscategories linux下将gbk文件转换为utf 8 2019 11 23 10 49 58 0800 编码 problems 转自Linux下GBK文件编码批量转换UTF 8命令 在工作中 xff0c
  • ubuntu上不了网的解决方法(简便)

    用于我们对于ubuntu的使用比较多 xff0c 网络有时需要更改设置 下面笔者分享一个实用的方法 1 增加多一个网络连接 将鼠标移至左上角上下箭头 xff0c 之后点击爱编辑连接 xff08 不同版本操作方法不一样但其实都是一样的操作路径
  • C++ 快读快输模板

    C 43 43 快读快输模板 快读 span class token keyword inline span span class token keyword int span span class token function read
  • sql server2008支持json函数以及2016版内置json函数

    一 sql server2008支持json函数 1 json 转化成数据集合 1 xff09 转化用函数 CREATE FUNCTION dbo parseJSON 64 JSON NVARCHAR MAX RETURNS 64 hier
  • 家庭宽带 动态公网IP,使用腾讯云DDNS实现实时更新DNS解析记录

    解决DDNS问题 动态公网IP 环境说明 xff1a 我是家庭宽带 只能申请到动态的公网ip xff0c 好处是不花钱 xff0c 弊端是每次重启光猫 xff0c 都会重新获取一个新的公网IP 为解决此办法 xff0c 我尝试了很多个DDN
  • 用c语言实现一个简单的计算器(数据结构)

    概要 xff1a 主要以c语言为例对数据结构中写一个简易的计算器 xff08 计算整数间加减乘除括号运算 xff09 的算法进行大致讲述 xff0c 细说如何去实现符号优先级的比较的函数实现 注 xff1a 由于编辑器缘故 xff0c 本文
  • apache网页中文乱码解决方法

    apache的配置文件 xff1a 进入httpd conf xff0c 在文件的最后添加AddDefaultCharset gb2312 xff0c 注意 xff0c 新版本的apache是没有默认的AddDefaultCharset字段
  • 将ubuntu设置为NAS——搭建Jellyfin影音服务器

    Jellyfin安装 前提是你已经安装好了docker docker ps 查看docker 安装容器 docker ps a 下载docker jellyfin的镜像文件 sudo docker pull jellyfin jellyfi
  • CommonJS概述及使用

    在web开发中 xff0c 我们的js文件都是在html文件中引入 xff0c 在浏览器环境运行 但是在nodejs的环境中可没有html文件 xff0c 且nodejs中 xff0c 有且只有一个入口文件index js xff08 在执
  • HDFS(一)HDFS基本介绍

    HDFS基本介绍 HDFS的Master Slave结构HDFS角色作用简介HDFS 分块存储抽象成数据块的好处HDFS 副本机制名字空间 xff08 NameSpace xff09 NameNode 功能DataNode 功能机架感知原理
  • C++借助宏来实现由枚举值得到枚举变量名的字符串

    定义一个枚举 enum color span class token punctuation span RED span class token operator 61 span span class token number 0 span
  • 阿里云配置域名CDN加速

    前置条件 1 xff1a 阿里云账号 2 xff1a 已备案的域名 3 xff1a 拥有公网IP的服务器 登录控制台 新增源站 xff08 也就是你的云服务器IP xff09 按提示配置完成后进入到CDN管理页面 复制CNAME列的加速域名
  • Mybatis-PageHelper自定义count查询

    1 问题描述 使用分页插件pagehelper实现分页功能 xff0c 有个接口查询速度慢 2 问题原因 排查问题发现是因为在分页时 xff0c 查询总条数的sql执行慢 xff0c 而查询总条数的sql是在分页插件执行时 xff0c 在主
  • Python Django API项目构建

    项目构建心得 Python Django项目构建心得一 基础1 1 虚拟环境1 1 1 安装1 1 2 创建虚拟环境1 1 3 激活 退出 虚拟环境 1 2 Django命令 二 项目构建2 1 创建项目2 2 构建分类配置文件2 3 安装
  • 华为服务器Linux系统配置

    华为服务器Linux系统配置 前言 此文档是为了初次使用华为H22M 03 CPU SSD服务器 Ubuntu系统安装与配置 xff08 注 xff1a 合适的显示屏还是比较重要的 xff0c 分辨率越高越优先 xff09 的用户所提供的一
  • Share 很喜欢的派大星图片

    今天给大家分享一波我很喜欢的派大星头像 早起的派大星 奔跑的派大星 卖萌的派大星 可爱的派大星 冬日里的派大星 行走的派大星 二货派大星 老板范儿派大星 呆楞的派大星 无奈的派大星嫌弃的派大星 努力工作的派大星 666的派大星 这是我们安全
  • redis+lua脚本实现秒杀扣减库存 & SpringBoot环境+Redisson客户端

    redis 43 lua脚本实现秒杀扣减库存 amp SpringBoot环境 43 Redisson客户端 前言为什么使用Redisson项目搭建maven配置编写Redisson配置类编写Application yml编写启动类编写测试