redis lua脚本实战秒杀和减库存的实现

目录
  • 前言
  • 1.redisson介绍
  • 2. redis lua脚本编写与执行
  • 3.redis减库存lua脚本
  • 4.实战
    • 4.1 减库存逻辑
    • 4.2 压测

前言

我们都知道redis是高性能高并发系统必不可少的kv中间件,它以高性能,高并发著称,我们常常用它做缓存,将热点数据或者是万年不变的数据缓存到redis中,查询的时候直接查询redis,减轻db的压力,分布式系统中我们也会拿它来做分布式锁,分布式id,幂等来解决一些分布式问题,redis也支持lua脚本,而且能够保证lua脚本执行过程中原子性,这就使得它的应用场景很多,也很典型,在redisson这个redis客户端中,它的各种分布式锁底层就是使用lua来实现的。本文主要是学习一下redis lua脚本的编写,以及在redisson这个redis客户端中是怎样使用的,实战一下秒杀场景redis减库存lua脚本的编写,并伪真实环境压测查看效果。

1.redisson介绍

redisson是一个redis的客户端,它拥有丰富的功能,而且支持redis的各种模式,什么单机,集群,哨兵的都支持,各种各样的分布式锁实现,什么分布式重入锁,红锁,读写锁,信号量的,然后它操作redis的常用数据结构就跟操作jdk的各种集合一样简单。
这里我们稍微演示下,不做过多的介绍,api毕竟只是个api,有意思的都在它背后各种原理。

maven依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.1</version>
</dependency>

创建RedissonClient对象

Config config = new Config();
config.useSingleServer()
        .setAddress("redis://xxx:xx");
RedissonClient redissonClient = Redisson.create(config);

它的功能超级多,下面只是列举了一些常用的,还有什么Bloom过滤器,队列等等。

// string
redissonClient.getBucket("name").set("zhangsan");
redissonClient.getBucket("name").get();

// hash
RMap<Object, Object> user = redissonClient.getMap("user");
user.put("name","zhangsan");
user.put("age",11);

// list
RList<String> names = redissonClient.getList("names");
names.add("zhangsan");
names.add("lisi");
names.add("wangwu");
// set
RSet<Object> nameSet = redissonClient.getSet("names");
nameSet.add("lisi");
nameSet.add("lisi");

//lock
RLock lock = redissonClient.getLock("lock");
lock.lock();
lock.unlock();

// 分布式id
RAtomicLong id = redissonClient.getAtomicLong("id");
long l = id.incrementAndGet();

2. redis lua脚本编写与执行

其实redis中的lua脚本并不难,你也不需要把lua语言再去重学一遍,全凭感觉就好了,使用的时候去查下语法就ok了。
脚本中就一个redis.call() 应该算是函数吧(方法也可以),比如我要使用lua脚本实现set动作,就可以这样写

return redis.call('set','name','zhangsan');

其实就是跟redis交互命令一个样子,再使用lua语言做一些条件分支,循环啥的,就完成了一些稍微复杂的逻辑。
下面自己写一遍扣减库存的逻辑,你就会这个玩意了。
上面是介绍了lua脚本的编写,下面我们介绍下这个执行。
不管是redis自己带的那个客户端,还是jedis,jediscluster,redistemplate,redisson这些客户端都是支持lua脚本api的,其实就是eval,evalsha,scriptload 这几个命令用的比较频繁,我这里把菜鸟教程上面关于介绍redis脚本命令截图过来

多说无益,这里直接使用redisson客户端实践一下。

public class RedisLua {
    private static  final Config config ;
    private static  final RedissonClient redisson ;
    static {
        config = new Config();
        config.useSingleServer()
                .setAddress("redis://ip:port");

        redisson =  Redisson.create(config);
    }
    public static void main(String[] args) throws InterruptedException {
        redisson.getBucket("name").set(11);
        RScript script = redisson.getScript();
        String result = script.eval(RScript.Mode.READ_ONLY, new StringCodec(),"return redis.call('get','name');", RScript.ReturnType.VALUE);
        System.out.println(result);
    }
}

可以看到就是使用getScript方法获取一个script对象,然后调用script 对象的eval方法,这个script对象其实还是用好多个方法的,evalSha等等,可以自己研究下。然后就是通过lua脚本获取我上面set进去的name值。
我们在看上面菜鸟教程对脚本命令的介绍的时候,还发现有key… arg…这些东西,这个我认为就是动态替换(传参)的。
比如我现在不获取name这个key的值了,我要获取age 的,或者是我要直接set一个值,这个时候我就可以lua脚本中有两个变量与你传的参数对等起来
KEYS[1] ARGV[1]
你可以看作是数组,不过它的位置是从1开始的。

RScript script = redisson.getScript();
List<Object> keys=new ArrayList<>();
keys.add("age");
String re = script.eval(
        RScript.Mode.READ_WRITE,
        new StringCodec(),
        "return redis.call('set',KEYS[1],ARGV[1]);",
        RScript.ReturnType.VALUE,
        keys,1);
Object age = redisson.getBucket("age").get();
System.out.println(age);

KEYS[1] 就对应这age, ARGV[1]就对应1,同理,KEYS[2] 就对应keys集合中的第二个元素。

3.redis减库存lua脚本

先介绍下下单减库存是怎么干的吧,其实一般库存有可用库存与预占库存,再下单的时候,就将可用库存减去你购买的商品数量看看是否是小于0,如果是小于0的话,说明库存不够了,就不让下单购买了,如果可用库存充足,可用库存减去购买商品数量,预占库存加上你购买商品数量,当用户超时未支付或者是手动取消订单的时候,就会去预占库减去用户购买商品数,可用库存加上商品数,其实还有一个已售库存,商家发货,已售库存加上商品数,预占减去商品数,大体上是这个逻辑。
现在可以想下,如果让你来实现下单预占库存的功能,你会怎么做,数据库三个库存字段这个不用说了。
首先你得先把可售库存查询出来,然后与购买商品数量进行比较,如果是可售库存大于这个购买商品数量,就可以购买,更新可售库存与预占库存。
如果上面这段代码逻辑你不加一些特殊手段的处理,那ok,高并发场景下绝对会出现超卖现象。
如果是秒杀场景呢?血亏。
这个时候我们可能会增加一些特殊手段来解决,比如说加锁,加分布式锁,将这一段的业务逻辑锁住,这个时候就不会出现那种超卖现象了, 但是这个中情况如果售罄的话,也会一直查询数据库。秒杀的时候,流量那么高,你不能让这么大的流量直接查库,如果商品售罄直接返回就可以了,不用再查询数据库了。
一般秒杀的时候,会将商品的库存同步推到redis中,流量过来的时候,会先扣减redis的库存,如果redis成功了,才扣减数据库中的库存,如果redis中的库存没了,直接返回就ok,这样大流量就不会直接冲击数据库了,那么redis要实现这段逻辑的话,就需要lua脚本的原子性了。
接下来我们就实现一下lua脚本扣减库存逻辑。

public static final String LOCK_STOCK_LUA=  "local counter = redis.call('hget',KEYS[1],ARGV[1]); \n" +
                                                "local result  = counter - ARGV[2];" +
                                                "if(result>=0 ) then \n" +
                                                "   redis.call('hset',KEYS[1],ARGV[1],result);\n" +
                                                "   redis.call('hincrby',KEYS[1],ARGV[3],ARGV[2]);\n" +
                                                "   return 1;\n" +
                                                "end;\n" +
                                                "return 0;\n";

我这里已经写好了,直接贴出来。
数据设计大体是这个样子的,使用hash数据结构
商品:{
“可售库存”:100,
“预占库存”:0,
“已售库存”:0
}

local counter = redis.call('hget',KEYS[1],ARGV[1]);

获取可售库存数量

local result  = counter - ARGV[2];
if(result>=0 ) then

可售库存减去要购买的商品数量,如果是大于0的话,说明库存还够。

redis.call('hset',KEYS[1],ARGV[1],result);
redis.call('hincrby',KEYS[1],ARGV[3],ARGV[2]);
return 1;

重新设置可售库存数量,增加预占库存,然后返回1
如果是库存不够的话,直接返回0了就。

4.实战

4.1 减库存逻辑

减库存逻辑其实就是先是用lua脚本减redis库存,如果成功再去减数据库中的真实库存,如果减redis库存失败,库存不足,就不会再走后面减真实库存的逻辑了。
这块的话,我是写了一个库存服务,实现了这段逻辑,但是总感觉有各种数据不一致的问题,当然不是超卖,而是少卖问题,这里就不发出来了。

4.2 压测

我们这个实战是在阿里云进行的
redis选的是容器服务,按秒计费,配置是0.5c1g
mysql也是选择的容器服务,配置是0.5c1g
库存服务是云服务器,按小时计费的那种,配置是2c4g,因为要部署多个服务跟实例,选择的比较大。
压测也是使用的阿里云的性能测试服务。

redis监控,可以看到,这点并发对redis来说就是毛毛雨。cpu才使用7%

云服务器这块手速有点慢,没截图出来,cpu跟内存都在50%左右。

mysql数据库,可以看到cpu飙上去了,内存飙上去了。

数据库数据:

可以发现并没有出现超卖现象。

到此这篇关于redis lua脚本实战秒杀扣减库存的实现的文章就介绍到这了,更多相关redis lua战秒杀扣减库存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 基于Redis+Lua脚本实现分布式限流组件封装的方法

    创建限流组件项目 pom.xml文件中引入相关依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springf

  • redis中lua脚本的简单使用

    一.背景 在使用redis的过程中,发现有些时候需要原子性去操作redis命令,而redis的lua脚本正好可以实现这一功能.比如: 扣减库存操作.限流操作等等. redis的pipelining虽然也可以一次执行一组命令,但是如果在这一组命令的执行过程中,需要根据上一步执行的结果做一些判断,则无法实现. 二.使用lua脚本 Redis中使用的是 Lua 5.1 的脚本规范,同时我们编写的脚本的时候,不需要定义 Lua 函数.同时也不能使用全局变量等等. 1.lua脚本的格式和注意事项 1.格式

  • Nginx利用Lua+Redis实现动态封禁IP的方法

    一.背景 我们在日常维护网站中,经常会遇到这样一个需求,为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单.对于黑名单之内的 IP ,拒绝提供服务. 本文给大家介绍的是Nginx利用Lua+Redis实现动态封禁IP的方法,下面话不多说了,来一起看看详细的介绍吧 二.架构 实现 IP 黑名单的功能有很多途径: 1.在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求: 2.在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者

  • 详解Redis中Lua脚本的应用和实践

    引言 前段时间组内有个投票的产品,上线前考虑欠缺,导致被刷票严重.后来,通过研究,发现可以通过 redis lua 脚本实现限流,这里将 redis lua 脚本相关的知识分享出来,讲的不到位的地方还望斧正. redis lua 脚本相关命令 这一小节的内容是基本命令,可粗略阅读后跳过,等使用的时候再回来查询 redis 自 2.6.0 加入了 lua 脚本相关的命令,EVAL.EVALSHA.SCRIPT EXISTS.SCRIPT FLUSH.SCRIPT KILL.SCRIPT LOAD,

  • 通过redis的脚本lua如何实现抢红包功能

    redis 脚本介绍 Redis从2.6版本开始,通过内嵌支持Lua环境 好处 减少网络开销.可以将多个请求通过脚本的形式一次发送,减少网络延迟 原子操作.redis将整个脚本当作一个整体去执行,中间不会被其他命令插入,无需担心脚本执行过程中会出现竞态条件 复用.客户端发送的脚本会永久保存在redis中,可以复用这一脚本 数据库表设计 简单两张表,一个红包表,一个红包领取记录表 CREATE TABLE `t_red_envelope` ( `id` bigint(20) NOT NULL AU

  • 基于Redis实现分布式锁的方法(lua脚本版)

    1.前言 在Java中,我们通过锁来避免由于竞争而造成的数据不一致问题.通常我们使用synchronized .Lock来实现.但是Java中的锁只能保证在同一个JVM进程内中可用,在跨JVM进程,例如分布式系统上则不可靠了. 2.分布式锁 分布式锁,是一种思想,它的实现方式有很多,如基于数据库实现.基于缓存(Redis等)实现.基于Zookeeper实现等等.为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件 互斥性:在任意时刻,只有一个客户端能持有锁. 不会发生死锁:即使客户端

  • 使用lua+redis解决发多张券的并发问题

    前言 公司有一个发券的接口有并发安全问题,下面列出这个问题和解决这个问题的方式. 业务描述 这个接口的作用是给会员发多张券码.涉及到4张主体,分别是:用户,券,券码,用户领取记录. 下面是改造前的伪代码. 主要是因为查出券码那行存在并发安全问题,多个线程拿到同几个券码.以下都是基于如何让取券码变成原子的去展开. public boolean sendCoupons(Long userId, Long couponId) { // 一堆校验 // ... // 查出券码 List<CouponCo

  • redis通过lua脚本,获取满足key pattern的所有值方式

    我们知道,redis提供了keys命令去获取所有满足格式的key,如我们键入命令 keys "user*" 将得到所有以user开头的key 然后执行 mget命令可以获取多个key的值,如 但如果满足条件的key过多,我们要将所有key拿到,再用mget去拿到所有值则为相对比较麻烦,因此可以借助xargs redis-cli keys "user*"|xargs redis-cli mget获取到所有key的值 也可以执行lua脚本local keys = red

  • redis lua脚本实战秒杀和减库存的实现

    目录 前言 1.redisson介绍 2. redis lua脚本编写与执行 3.redis减库存lua脚本 4.实战 4.1 减库存逻辑 4.2 压测 前言 我们都知道redis是高性能高并发系统必不可少的kv中间件,它以高性能,高并发著称,我们常常用它做缓存,将热点数据或者是万年不变的数据缓存到redis中,查询的时候直接查询redis,减轻db的压力,分布式系统中我们也会拿它来做分布式锁,分布式id,幂等来解决一些分布式问题,redis也支持lua脚本,而且能够保证lua脚本执行过程中原子

  • Redis Lua脚本实现ip限流示例

    目录 引言 相比Redis事务来说,Lua脚本有以下优点 Lua脚本 java代码 IP限流Lua脚本 引言 分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能.首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数.Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法.如下操作因是在一个lua脚本中(相当于原

  • Redis+Lua脚本实现计数器接口防刷功能(升级版)

    目录 [前言] [实现过程] 一.问题分析 二.解决方案 三.代码改造 [总结] [前言] Cash Loan(一):Redis实现计数器防刷中介绍了项目中应用redis来做计数器的实现过程,最近自己看了些关于Redis实现分布式锁的代码后,发现在Redis分布式锁中出现一个问题在这版计数器中同样会出现,于是融入了Lua脚本进行升级改造有了Redis+Lua版本. [实现过程] 一.问题分析 如果set命令设置上,但是在设置失效时间时由于网络抖动等原因导致没有设置成功,这时就会出现死计数器(类似

  • Redis高并发情况下并发扣减库存项目实战

    目录 第一种方案:纯MySQL扣减实现 MySQL架构升级 第二种方案:缓存实现扣减 第三种方案:数据库+缓存 顺序写的性能更好 顺序写的架构 扣减流程 相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等.做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的.这里我今天就讲一下高并发情况下是如何扣减库存的? 我们对扣减库存所需要关注的技术点如下: 当前剩余的数量大于等于当前需要扣减的数量,不

  • 简介Lua脚本与Redis数据库的结合使用

    可能你已经听说过Redis 中嵌入了脚本语言,但是你还没有亲自去尝试吧?  这个入门教程会让你学会在你的Redis 服务器上使用强大的lua语言. Hello, Lua! 我们的第一个Redis Lua 脚本仅仅返回一个字符串,而不会去与redis 以任何有意义的方式交互. 复制代码 代码如下: local msg = "Hello, world!" return msg 这是非常简单的,第一行代码定义了一个本地变量msg存储我们的信息, 第二行代码表示 从redis 服务端返回msg

  • Redis调用Lua脚本及使用场景快速掌握

    目录 一.阅读本文前置条件 二.为什么需要Lua脚本 三.学点Lua语法 3.1.一个简单的例子 3.2.仔细看下Lua脚本里的内容 3.3.复杂点的例子 四.Lua脚本预加载 五.一个修改JSON数据的例子? 六.总结 一.阅读本文前置条件 可以遵循这个链接中的方法在操作系统上安装 Redis 如果你对redis命令不熟悉,查看<Redis 命令引用> 二.为什么需要Lua脚本 简而言之:Lua脚本带来性能的提升. 很多应用的服务任务包含多步redis操作以及使用多个redis命令,这时你可

  • Redis中lua脚本实现及其应用场景

    目录 1. Redis Lua脚本概述 2. Redis Lua脚本的优势 3. Redis Lua脚本的应用场景 4. Redis Lua脚本的使用方法 5. java中使用redis的lua脚本 5.1. 添加Redis依赖 在pom.xml中添加以下依赖: 5.2. 配置Redis连接信息 在application.properties中添加以下配置: 5.3. 定义Redis Lua脚本 5.4. 实现RedisService 5.5. 编写Redis Lua脚本 5.6. 测试Redi

  • 基于Redis结合SpringBoot的秒杀案例详解

    目录 1.构建SpringBoot项目 2.启动类 3.在Controller层里定义秒杀接口 4.在Service层里通过lua脚本实现秒杀效果 5.配置redis连接参数 6.演示秒杀效果 6.1 准备redis环境 6.2 启动项目 6.3 多线程形式发起秒杀请求 1.构建SpringBoot项目 搭建名为quickbuy的springboot项目,相关的依赖包如下所示: <?xml version="1.0" encoding="UTF-8"?>

随机推荐