优惠券秒杀项目

news2024/9/23 15:33:24

一、添加优惠券的同时,将优惠券信息,以及用户列表放到redis中

@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);

    // 保存秒杀内容放到 Redis 中,包括库存、开始时间和结束时间,使用 hash
    Map<String, String> voucherData = new HashMap<>();
    voucherData.put("stock", String.valueOf(voucher.getStock()));
    voucherData.put("beginTime", String.valueOf(voucher.getBeginTime().toInstant(ZoneOffset.UTC).toEpochMilli()));
    voucherData.put("endTime", String.valueOf(voucher.getEndTime().toInstant(ZoneOffset.UTC).toEpochMilli()));

    stringRedisTemplate.opsForHash().putAll("seckill:voucher:" + voucher.getId(), voucherData);

    // 加一个 set 集合,用来放抢到优惠券的用户 ID,防止重复抢
    stringRedisTemplate.opsForSet().add("seckill:voucher:users:" + voucher.getId(), "0");
}

image-20240822163631066

使用Redis生成订单ID:

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        //如果不存在,会自动创建然后开始自增
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

二、使用lua查看是否满足购买条件

2.1 编写lua脚本

-- KEYS 和 ARGV 是 Redis 提供的两种参数类型

-- KEYS[1] 是秒杀活动的哈希表键,包含库存、开始时间和结束时间
-- KEYS[2] 是记录用户ID的集合,防止重复购买

-- ARGV[1] 是用户ID,用于检查该用户是否已经购买过
-- ARGV[2] 是当前时间的时间戳(秒),用于与秒杀活动的开始和结束时间进行比较

-- 获取秒杀活动的信息
local stock = redis.call('hget', KEYS[1], 'stock')
local beginTime = redis.call('hget', KEYS[1], 'beginTime')
local endTime = redis.call('hget', KEYS[1], 'endTime')

-- 当前时间
local currentTime = tonumber(ARGV[2])

-- 检查活动是否在有效时间内
beginTime = tonumber(beginTime)
endTime = tonumber(endTime)

if currentTime < beginTime then
    return -4  -- 活动尚未开始
end

if currentTime > endTime then
    return -5  -- 活动已经结束
end

-- 检查库存是否足够
stock = tonumber(stock)

if stock <= 0 then
    return -2  -- 库存不足
end

-- 获取用户ID
local userId = ARGV[1]

-- 检查用户是否已经购买过
local userExists = redis.call('sismember', KEYS[2], userId)
if userExists == 1 then
    return -3  -- 用户已经购买过
end

-- 扣减库存
redis.call('hincrby', KEYS[1], 'stock', -1)

-- 记录用户购买信息
redis.call('sadd', KEYS[2], userId)

-- 成功,返回 1
return 1

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    ISeckillVoucherService seckillVoucherService;

    @Autowired
    RedisIdWorker redisIdWorker;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 秒杀 Lua 脚本, 当类加载的时候就执行
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    @Override
    public Result seckillVoucher(Long voucherId) {

        // 1. 使用 Redis 的 Lua 脚本,查看是否满足购买条件,包括时间、库存、用户是否已经购买
        String voucherKey = "seckill:voucher:" + voucherId;
        String userKey = "seckill:voucher:users:" + voucherId;
        long currentTimeMillis = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
        long userId = 1L; // 从用户会话中获取

        // 2. 调用 Lua 脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT, // Lua 脚本
                Arrays.asList(voucherKey, userKey), // Key 列表
                String.valueOf(userId), // 参数列表:用户 ID 转为字符串
                String.valueOf(currentTimeMillis) // 参数列表:当前时间转为字符串
        );

        // 3. 处理 Lua 脚本返回的结果
        if (result != null) {
            int r = result.intValue();

            switch (r) {
                case -2:
                    return Result.fail("库存不足!");
                case -3:
                    return Result.fail("您已购买过该优惠券!");
                case -4:
                    return Result.fail("秒杀活动尚未开始!");
                case -5:
                    return Result.fail("秒杀活动已经结束!");
                case 1:
                    // 成功抢购,将订单信息发送到 RabbitMQ 队列中
                    VoucherOrder voucherOrder = createOrder(voucherId, userId);
                    rabbitTemplate.convertAndSend("seckill.direct", "order.routing.key", voucherOrder);
                    return Result.ok("抢购成功,订单正在处理中!");
                default:
                    return Result.fail("未知错误!");
            }
        }

        return Result.fail("操作失败!");
    }

    // 创建订单
    private VoucherOrder createOrder(Long voucherId, Long userId) {
        VoucherOrder order = new VoucherOrder();
        order.setVoucherId(voucherId);
        order.setUserId(userId);
        order.setId(redisIdWorker.nextId("order")); // 生成订单 ID
        return order;
    }
}
package com.hmdp.consumer;

import com.hmdp.entity.VoucherOrder;
import com.hmdp.service.IVoucherOrderService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.QueueBinding;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class OrderConsumer {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private IVoucherOrderService voucherOrderService;


    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "seckill.order.queue"),
            exchange = @Exchange(name = "seckill.direct", type = ExchangeTypes.DIRECT),
            key = "order.routing.key"
    ))
    public void processOrder(VoucherOrder order) {

        String lockKey = "order:lock:" + order.getId();
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean isLockAcquired = lock.tryLock(10, 5, TimeUnit.SECONDS);

            if (isLockAcquired) {
                try {
                    voucherOrderService.save(order);
                } finally {
                    lock.unlock();
                }
            } else {
                // 无法获取锁,可能日志记录
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // 错误处理
        } catch (Exception e) {
            // 错误处理
        }
    }

}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hmdp</groupId>
    <artifactId>hm-dianping</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>hm-dianping</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

        <!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!--redission-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

        <!--fastjson-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.10</version>
        </dependency>

    </dependencies>



    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
server:
  port: 8081
spring:
  application:
    name: hmdp
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC
    username: root
    password: root
  redis:
    host: 192.168.0.103
    port: 6379
    password: 123
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
  rabbitmq:
    host: 192.168.0.103 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /seckill # 虚拟主机
    username: seckill # 用户名
    password: 123123 # 密码


  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.hmdp.entity # 别名扫描包
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.hmdp: debug
    org.springframework.amqp: DEBUG

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2068342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux dig域名DNS 查询与iptables域名ip访问流量限制;PTR 反向解析从 IP 地址到域名的映射

一、域名 dns查询 在 Linux 系统中&#xff0c;你可以使用多种工具和技术来进行 DNS 查询和 IP 限制。以下是一些常用的方法和工具&#xff1a; DNS 查询 dig 命令&#xff1a; dig 是一个强大的命令行工具&#xff0c;用于查询 DNS 信息。 dig example.com你可以指定查询类型…

【TCP】核心机制:滑动窗口、流量控制和拥塞控制

文章目录 滑动窗口窗口滑动滑动窗口丢包 流量控制拥塞控制窗口大小变化过程 滑动窗口 有一类算法题&#xff0c;就是通过滑动窗口的思想来解决的&#xff0c;算法中的“滑动窗口”借鉴自 TCP 的滑动窗口 TCP 是要保证可靠传输的>代价&#xff0c;降低了传输的效率&#xf…

【机器学习】3. 欧式距离,曼哈顿距离,Minkowski距离,加权欧式距离

Euclidean - L2 norm L2范数 D ( A , B ) ( a 1 − b 1 ) 2 ( a 2 − b 2 ) 2 . . . D(A,B) \sqrt{(a_1-b_1)^2(a_2-b_2)^2 ...} D(A,B)(a1​−b1​)2(a2​−b2​)2... ​ Manhattan D ( A , B ) ∣ a 1 − b 1 ∣ ∣ a 2 − b 2 ∣ . . . D(A,B) \sqrt{|a_1-b_1||a_…

全网最简单的Java设计模式【九】策略模式-实战中最常用的设计模式之一

策略模式是一种行为设计模式&#xff0c;它允许你定义一系列的算法&#xff0c;把它们一个个封装起来&#xff0c;并且使它们可以相互替换。该模式让算法的变化独立于使用算法的客户。在实际开发中&#xff0c;策略模式可以帮助我们减少大量的 if-else 或 switch 条件判断语句&…

产品的需求分析

一、需求 1.需求概念 用户的预期与现实存在在差异 用户所期望的明确的解决方案 2.需求的常用形式 提问题 提目的 提方案 3.需求的来源 外部需求&#xff1a;用户、市场、其他竞品 内部需求&#xff1a;业务部门、产品经理 4.需求测试方法 定性方式&#xff1a;逻辑…

【生日视频制作】江边夜景游轮观光船霓虹灯AE模板修改文字软件生成器教程特效素材【AE模板】

游轮观光船生日视频制作教程AE模板修改文字特效软件生成器素材 怎么如何做的【生日视频制作】江边夜景游轮观光船霓虹灯AE模板修改文字软件生成器教程特效素材【AE模板】 生日视频制作步骤&#xff1a; 安装AE软件下载AE模板把AE模板导入AE软件修改图片或文字渲染出视频

鸿蒙(API 12 Beta3版)【使用Image完成图片接收器】图片开发指导依赖JS对象

图像接收类&#xff0c;用于获取组件surface id&#xff0c;接收最新的图片和读取下一张图片&#xff0c;以及释放ImageReceiver实例。 开发步骤 添加依赖 在进行应用开发之前&#xff0c;开发者需要打开native工程的src/main/cpp/CMakeLists.txt&#xff0c;在target_link_…

【Qt】Qt系统 | Qt事件| 鼠标事件

文章目录 鼠标事件鼠标点击事件鼠标释放事件鼠标双击事件鼠标移动事件 滚轮事件 在 Qt 中&#xff0c;鼠标事件是用 QMouseEvent 实现的。当在窗口中按下鼠标或者移动鼠标时&#xff0c;都会产生鼠标事件 鼠标事件 鼠标点击事件 鼠标按下时通过 虚函数 mousePressEvent() 来…

系统编程—进程

一、进程的概念 1.程序与进程的区别 程序:编译后产生的&#xff0c;格式为ELF的&#xff0c;存储于硬盘的文件。可以通过 readelf -s [文件名] 查看文件信息 …

使用OpenRewrite升级SpringBoot项目

使用OpenRewrite升级SpringBoot项目 什么是OpenRewrite? OpenRewrite是一个强大的开源自动化源代码重构工具,专为大规模代码库设计。它的核心理念是通过程序化的方式来改变源代码,而不是依赖于手动编辑或简单的查找替换。 OpenRewrite的主要特点和优势包括: 自动重构: Open…

Unity动画模块 之 Animator中一些常见参数

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 我发现我忘了写Animator了&#xff0c;正好有些不常用的参数还没怎么认识,笔记来源于唐老狮 1.状态窗口参数 2.连线参数…

Lesson05--C/C++内存管理

1. C/C内存分布 2. C语言中动态内存管理方式 3. C中动态内存管理 4. operator new与operator delete函数 5. new和delete的实现原理 6. 定位new表达式(placement-new) 7. 常见面试题 1. C/C内存分布 为了方便管理内存被分为了以上的区域 我们来看下面的一段代码和相关问题 …

数据结构(邓俊辉)学习笔记】优先级队列 08——左式堆:结构

文章目录 1. 第一印象2. 堆之合并3. 奇中求正4. NPL5. 左倾性6. 左展右敛 1. 第一印象 在学习过常规的完全二叉堆之后&#xff0c;我们再来学习优先级队列的另一变种&#xff0c;也就是左式堆。所谓的左式堆&#xff0c;也就是在拓扑形态上更加倾向于向左侧倾斜的一种堆&#…

Vue3 项目结构

1.main.ts 2.简单写一个src下的结构 App.vue 根组件 <template><div class"app"><!-- html --><h1>你好啊!</h1></div> </template><script lang"ts"> //js 或 tsexport default {name:App,//组件名 }…

MySQL数据库锁机制(全面讲解)

目录 1、全局锁 1.1、全局锁使用语法 1.2、备份数据库&#xff08;不使用全局锁&#xff09; 2、表锁 2.1、读写锁 读锁 写锁 2.2、元数据锁&#xff08;meta data lock MDL&#xff09; 2.3、意向锁 3、行锁 3.1、共享锁和排他锁 共享锁&#xff08;S锁&#xff…

原来marker还能这么玩

在Web GIS开发中&#xff0c;Marker&#xff08;标记&#xff09;是一个基本但强大的工具。今天&#xff0c;我们将探讨如何通过不同的API调用&#xff0c;将Marker玩出新花样&#xff0c;让地图更加生动有趣。 最基础的Marker用法是在地图上标记一个具体位置。我们可以通过“m…

【Unity3D小技巧】Unity3D中实现对InputField的自定义输入限制实例

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址QQ群&#xff1a;398291828 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 InputField是UGUI的文本输入框&#xff0c;自带的…

粘包,Telnet,SSH,Wireshark

一&#xff0c;粘包 原因&#xff1a;tcp为流式套接字&#xff0c;数据与数据间没有边界&#xff0c;导致多次数据粘到一起。 解决&#xff1a; 1.规定一些数据间的间隔符&#xff0c;"\aa","\r\n"; 2.可以指定要发送对象的数据长度。 3.自己将数据打包。 …

力扣:有效的数独

文章目录 需求分析结尾 需求 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08…

2024年最新最全的【大模型学习路线规划】从零基础入门到精通!

2024年最新最全的大模型学习路线规划&#xff0c;对于零基础入门到精通的学习者来说&#xff0c;可以遵循以下阶段进行&#xff1a; 文章目录 一、基础准备阶段数学基础&#xff1a;编程语言&#xff1a;深度学习基础&#xff1a; 二、核心技术学习阶段Transformer模型&#xf…