redis 结合Lua脚本实现 秒杀、防止超卖

发布于:2024-04-27 ⋅ 阅读:(128) ⋅ 点赞:(0)

需求:
同1商品单个用户限购1件,库存不会超卖

1 Lua脚本,因可实现原子性操作,这个文件放到resources目录下

local userId = KEYS[1] -- 当前秒杀的用户 ID
local goodsId = KEYS[2] -- 秒杀的商品 ID
-- 订单id
local orderId = ARGV[1]

redis.log(redis.LOG_NOTICE,"秒杀商品ID:‘"..goodsId.."’,当前秒杀用户 ID:‘"..userId.."’") -- 日志记录

-- 使用一个统一的前缀来存储所有商品的库存信息
local stockHashKey = "Seckill:Stock" -- 秒杀商品的库存哈希KEY

-- 如果一个用户已经参加过秒杀了,那么不应该重复参加
-- 所有的秒杀的商品一定要保存在 SET 集合(用户 ID 不能重复)
local resultKey = "Seckill:Result:"..goodsId
local resultExists = redis.call('SISMEMBER', resultKey, userId)
redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】当前用户参加秒杀的状态:"..resultExists)

if tonumber(resultExists) == 1 then
    return -1 -- 用户参加过秒杀了
else
    -- 获取当前商品库存数量,使用HGET命令从哈希表中获取
    local goodsCount = redis.call('HGET', stockHashKey, goodsId)
    if goodsCount == false then
        goodsCount = 0 -- 如果没有这个字段,默认库存为0
    end
    redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】当前商品库存量:"..goodsCount)
    if tonumber(goodsCount) <= 0 then -- 商品抢光了
        redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】用户秒杀失败。")
        return 0
    else -- 还有库存
        -- 更新库存数量,使用HINCRBY命令减少库存
        redis.call('HINCRBY', stockHashKey, goodsId, -1)
        -- 秒杀结果记录
        redis.call('SADD', resultKey, userId)
        -- 发送一条消息到stream队列中
        redis.call('xadd', 'Seckill:orders_queue', '*', 'userId', userId, 'goodsId',goodsId,'orderId', orderId)
        redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId .."】用户秒杀成功。")
        return 1
    end
end

2 写个配置类,读取Lua脚本

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

@Configuration
public class LuaConfiguration {

    @Bean(value = "seckill_stockScript")
    public DefaultRedisScript<Long> miaosha_stockScript() {
        //脚本的范型返回值是Long
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //放在resources目录下
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("sha_2.lua")));

        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

3 主程序逻辑

import lombok.extern.slf4j.Slf4j;
import org.example.service_a.service_a_App;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.List;

@SpringBootTest(classes = {seckillService.class})
@Slf4j
public class Test_2_lua {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Qualifier(value = "seckill_stockScript")
    @Autowired
    private DefaultRedisScript<Long> redisScript;

    /**
     * 这里没有写模拟压测的,只写了核心调用逻辑
     * 可以写个线程池批量压,或是jmeter压
     */
    @Test
    public void executeLuaScriptFromFile() {

        /**
         * 准备Lua脚本所需参数,如 KEYS,ARGV
         * 这个脚本 KEYS 为 用户id,商品id
         * ARGV 为 订单id
         * 这些通常是键名,用于在脚本中作为变量使用
         */
        List<String> keys = Arrays.asList("user_1", "goodsId_1");

        // hutool 工具类雪花算法,默认调用的是getSnowflake()方法,生成id
        // createSnowflake(long workerId, long datacenterId) 方法,是每次调用都会创建一个新的Snowflake对象,不同的Snowflake对象创建的ID可能会有重复,不推荐
        String orderId = IdUtil.getSnowflakeNextIdStr();
        //脚本里返回1说明秒杀成功
        Long execute = stringRedisTemplate.execute(redisScript, keys,orderId );
    }
}

4 redis准备测试数据,这里模拟了些数据

#用hash结构存储商品id和库存数量,默认10个库存
hset Seckill:Stock goodsId_1 10
hset Seckill:Stock goodsId_2 10
hset Seckill:Stock goodsId_3 10

#用set结构存储某个商品id中,秒杀成功的用户id列表
del Seckill:Result:goodsId_1
del Seckill:Result:goodsId_2
del Seckill:Result:goodsId_3

# stream类型的队列,有产品id,用户id,订单id
del Seckill:orders_queue

keys *

hget Seckill:Stock goodsId_1
hget Seckill:Stock goodsId_2
hget Seckill:Stock goodsId_3

# 查看stream队列中的消息
xrange Seckill:orders_queue - +


 


网站公告

今日签到

点亮在社区的每一天
去签到