秒杀和下单业务中Redis+Lua脚本解决并发安全问题

前言:

关于Lua脚本介绍:Lua脚本的作用是可以执行一系列Redis命令,将它们封装在一个原子操作中,确保这些命令的执行是连续的且不会被其他客户端的操作中断。这样可以保证一系列操作的原子性,避免出现数据不一致的情况。

 场景介绍:当我们下单操作后,需要对Redis中的库存信息进行删减,虽然Redis是线程安全的,但在JAVA中下单操作是分为两个步骤,首先是先要判断存入Redis中的库存是否充足,充足后再进行扣减,此处查询库存和扣减库存是两步操作,所以在这两步操作之间,就有可能在并发情况下导致线程安全问题而导致“超卖等现象”。

以下图为例,假设此时商品只剩最后一个,这时有两个并发线程进来,首先进行Redis查询后判断库存,这时候线程切换到线程二,线程二也执行查询操作(此时线程一还没有扣减数据),判断后扣减,然后回到线程一,扣减数据。这时候就发生了“超卖”问题。

Lua脚本解决方案

在上述环境中会发生超卖问题是因为查询和写数据分成了两步操作,因为线程轮巡而导致数据不一致问题,而Lua脚本则可以将查询Redis操作和扣减数据库操作的命令合并成一个原子操作,这样就不需要在JAVA中再进行处理而导致线程安全问题了。

Redis+Lua脚本

Redis中后面版本自带了Lua脚本的解释器,所以无需再进行下载。

进入Redis容器使用SCRIPT EXISTS查看是否支持Lua脚本(返回一个空数组)

Lua脚本基础语法

Lua中定义变量语法是local 变量名

local userid=0

判断语法

💡注意事项:

在Lua脚本中数组下标从[1]开始,即相当于JAVA的[0], 字符串拼接是使用 .. str1 .. str2

秒杀业务Lua脚本示例:

local resultFlag = "0" 
local n = tonumber(ARGV[1]) 
local key = KEYS[1] 
local goodsInfo = redis.call("HMGET",key,"totalCount","seckillCount") 
local total = tonumber(goodsInfo[1]) 
local alloc = tonumber(goodsInfo[2]) 
if not total then 
    return resultFlag 
end 
if total >= alloc + n  then 
    local ret = redis.call("HINCRBY",key,"seckillCount",n) 
    return tostring(ret) 
end 
return resultFlag

首先Lua脚本调用Redis命令使用redis.call()进行调用,方法中第一个参数为redis的命令,如set、get等,上面示例的HMGETHINCRBY表示是对哈希数据结构的命令,

HMGET命令用于获取哈希(Hash)数据结构中一个或多个字段的值。语法为:HMGET key field1 (field2 ... fieldN)

对应上述示例: redis.call("HMGET",key,"totalCount","seckillCount")

HINCRBY命令用于将哈希数据结构中指定字段的值增加指定的增量。语法为:HINCRBY key field increment

对应上述示例: redis.call("HINCRBY",key,"seckillCount",n)

KEYS[1] 和ARGV[1]表示传入参数的第一个key和对应的value值

在Redis中执行语法:EVAL script numkeys key [key …] arg [arg …],而我们是通过JAVA语句调用,需要注意的就是传入的key和对应的arg(value)。就相当于一个K-V结构,传入一个Key就会对应一个Value,传两个Key,就会对应两个Value。

RedisTemplate操作Lua脚本

Java代码示例:

public int secKill(String id, int number) { 
    String key = getCacheKey(id); 
    Object seckillCount =  redisTemplate.execute(script, Arrays.asList(key), String.valueOf(number)); 
    return Integer.valueOf(seckillCount.toString()); 
}

可以使用redisTemplate.execute()方法来执行Lua脚本,其中第一个参数为script即lua脚本,第二个参数根据脚本中的KEYS[]和ARGV[]来定,前提是需要将key转为List结构,ARGV需要转为String类型。

编写好Lua脚本后用java代码拼接(也可以直接创建Lua脚本文件)

先要创建一个Script对象,然后拼接好的Lua脚本转为字符串传入第一个参数,第二个参数是指返回结果类型。

RedisScript<String> redisScript=new DefaultRedisScript<>(sb.toString(),String.class);

然后执行 redisTemplate.execute(redisScript, Arrays.asList(key),num);

@Test
    public void testRedisLua(){
        StringBuilder sb=new StringBuilder();
        //传入的参数应该是商品的哈希key
        //哈希结构key-商品id
        //totalCount   100  总库存
        //seckillCount  0     已经售出数量
        sb.append("local resultFlag=\"0\"\n");
        //传入的参数key   --对应商品id key
        sb.append("local key=KEYS[1]\n");
        //传入的参数arg   --对应订购的数量
        sb.append("local addCap=tonumber(ARGV[1])\n");
        //先将key进行查询判断库存是否充裕
        sb.append("local phoneInfo=redis.call(\"HMGET\",key,\"totalCount\",\"seckillCount\")\n");
        sb.append("local total=tonumber(phoneInfo[1])\n");//得到totalCount 总库存数
        sb.append("local alsell=tonumber(phoneInfo[2])\n");//得到seckillCount 已经售出的数量
        //如果没有值则返回 “0”
        sb.append("if not total then\n");
        sb.append("return resultFlag\n");
        sb.append("end\n");
        //如果库存数量大于已售出数量加需要订购的数量(代表还有货)则将已售出数量增加订购数量并返回
        sb.append("if total >=alsell + addCap then\n");
        //将售出数量进行增加(订购数量)
        sb.append("local ret=redis.call(\"HINCRBY\",key,\"seckillCount\",addCap)\n");
        sb.append("return tostring(ret)\n");
        sb.append("end\n");
        //如果没货返回 "0"
        sb.append("return resultFlag\n");
        RedisScript<String> redisScript=new DefaultRedisScript<>(sb.toString(),String.class);

        redisTemplate.execute(redisScript, Arrays.asList(key),num);
    }

采用Lua文件进行执行(推荐)

在resources目录下创建好lua脚本文件

private static final DefaultRedisScript<String> SECKILL_KEY;

static {
    SECKILL_KEY=new DefaultRedisScript<>();
    SECKILL_KEY.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_KEY.setResultType(String.class);//返回结果类型
}

可以直接通过文件读取,而不是使用StringBuffer进行字符串拼接。

到此整个流程的基本内容已经介绍完毕,大家可以根据自己项目不同的业务去进行实现或者测试。此方案适用于单节点的Redis,在读写分离以及集群状况下需要进一步设计。

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值