从零搭建基于SpringBoot的秒杀系统(四):雪花算法生成订单号以及抢购功能实现

简介: 抢购功能是整个系统的核心,接下来的很多优化都是在优化抢购功能,在写抢购功能模块之前,先封装几个公共的类。

一、公共状态类封装

先想一下抢购逻辑,点击购买按钮后,通过post请求将数据传递给接口,接口返回成功或失败信息。因此我们需要先封装一个类描述返回信息,在response文件夹下新建BaseResponse,包含一个状态码,成功失败信息以及数据


package com.sdxb.secondkill.response;
import com.sdxb.secondkill.enums.StatusCode;
public class BaseResponse<T> {
    private Integer code;
    private String msg;
    private T data;
    public BaseResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
    public BaseResponse(StatusCode code) {
        this.code = code.getCode();
        this.msg = code.getMsg();
    }
    public BaseResponse(Integer code, String msg) {
        this.code=code;
        this.msg=msg;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    @Override
    public String toString() {
        return "BaseResponse{" +
                "code=" + code +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }
}


BaseResponse中的状态码和成功失败信息我们通过枚举类来总结,在enums中新建一个StatusCode


package com.sdxb.secondkill.enums;
public enum StatusCode {
    //表示成功
    Success(0,"成功"),
    //表示失败
    Fail(-1,"失败"),
    //表示参数非法
    InvalidParam(201,"非法的参数"),
    //表示用户未登录
    UserNotLog(202,"用户未登录"),
    ;
    private Integer code;
    private String msg;
    StatusCode(Integer code,String msg){
        this.code=code;
        this.msg=msg;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}


接下来就可以通过BaseResponse来返回接口的成功或失败信息。

再写一个枚举类用于记录订单支付的状态,在enums下新建SysConstant


public class SysConstant {
    public enum OrderStatus{
        //订单无效
        Invalid(-1,"无效"),
        //订单成功未付款
        SuccessNotPayed(0,"成功-未付款"),
        //订单已付款
        HasPayed(1,"已付款"),
        //订单已取消
        Cancel(2,"已取消"),
        ;
        private Integer code;
        private String msg;
        OrderStatus(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
        public Integer getCode() {
            return code;
        }
        public void setCode(Integer code) {
            this.code = code;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
}


二、抢购业务逻辑编写

2.1 使用雪花算法生成订单编号

高并发环境下需要快速生成唯一且递增的订单编号,这个ID需要全局唯一,为了防止ID冲突可以使用36位的UUID,但是UUID有以下缺点:


1.UUID字符串占用的空间比较大。

2.索引效率很低。

3.生成的ID很随机,不是人能读懂的。

4.做不了递增,如果要排序的话,基本不太可能。

这里就可以用雪花算法来解决订单编号的问题:雪花算法是推特开源的分布式id生成算法,雪花算法的具体原理我们不做介绍,只要知道它可以在硬件级别上快速生成递增id就可以了,直接放代码:

在utils文件夹上新建一个SnowFlake类:



package com.sdxb.secondkill.utils;
public class SnowFlake {
    /**
     * 起始的时间戳
     */
    private final static long START_STAMP = 1480166465631L;
    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5;//数据中心占用的位数
    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
    private long dataCenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStamp = -1L;//上一次时间戳
    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }
    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStamp = getNewStamp();
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
        if (currStamp == lastStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }
        lastStamp = currStamp;
        return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分
                | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }
    private long getNextMill() {
        long mill = getNewStamp();
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }
    private long getNewStamp() {
        return System.currentTimeMillis();
    }
    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            System.out.println("当前生成的有序数字串:"+snowFlake.nextId());
        }
        System.out.println("总共耗时:"+(System.currentTimeMillis() - start));
    }
}


我在类中写了一个main方法测试生成100万个id的速度,生成100万个id一共花费4.4秒



三、抢购处理逻辑编写

抢购逻辑中需要编写两项DTO类,KillDto中保存订单id和用户id,抢购时就通过这两个数据来发起抢购,dto下新建KillDto:


@Data
@ToString
public class KillDto implements Serializable {
    private Integer killid;
    private Integer userid;
    public KillDto() {
    }
    public KillDto(Integer killid, Integer userid) {
        this.killid = killid;
        this.userid = userid;
    }
}


在Controller文件下新建一个KillController,用来接受抢购请求


package com.sdxb.secondkill.controller;
import com.sdxb.secondkill.enums.StatusCode;
import com.sdxb.secondkill.response.BaseResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
@Controller
public class KillController {
    private static final String prefix="kill";
    @Autowired
    private KillService killService;
    @RequestMapping(value = prefix+"/execute",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public BaseResponse execute(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
        if (result.hasErrors()||killDto.getKillid()<0){
            return new BaseResponse(StatusCode.InvalidParam);
        }
        //未创建登陆模块前先默认为10
        Integer userid=10;
        try {
            Boolean res=killService.KillItem(killDto.getKillid(),userid);
            if (!res){
                return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
        return baseResponse;
    }
    @RequestMapping(value = prefix+"/execute/success",method = RequestMethod.GET)
    public String killsuccess(){
        return "killsuccess";
    }
    @RequestMapping(value = prefix+"/execute/fail",method = RequestMethod.GET)
    public String killfail(){
        return "killfail";
    }
}


所有的业务处理都放到Service中去处理,在Service文件下创建KillService接口


public interface KillService {
    Boolean KillItem(Integer killId,Integer userId) throws Exception;
}


在Service下的Impl文件夹中创建KillServiceImpl类,用于处理详细的业务:


@Service
public class KillServiceImpl implements KillService {
    private SnowFlake snowFlake=new SnowFlake(2,3);
    @Autowired
    private ItemKillMapper itemKillMapper;
    @Autowired
    private ItemKillSuccessMapper itemKillSuccessMapper;
    public Boolean KillItem(Integer killId, Integer userId) throws Exception {
        Boolean result=false;
        //判断当前用户是否抢购过该商品
        if (itemKillSuccessMapper.countByKillUserId(killId,userId)<=0){
            //获取商品详情
            ItemKill itemKill=itemKillMapper.selectByid(killId);
            if (itemKill!=null&&itemKill.getCanKill()==1){
                int res=itemKillMapper.updateKillItem(killId);
                if (res>0){
                    commonRecordKillSuccessInfo(itemKill,userId);
                    result=true;
                }
            }
        }else {
            System.out.println("您已经抢购过该商品");
        }
        return result;
    }
    private void commonRecordKillSuccessInfo(ItemKill itemKill, Integer userId) {
        ItemKillSuccess entity=new ItemKillSuccess();
        String orderNo=String.valueOf(snowFlake.nextId());
        entity.setCode(orderNo);
        entity.setItemId(itemKill.getItemId());
        entity.setKillId(itemKill.getId());
        entity.setUserId(userId.toString());
        entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
        entity.setCreateTime(DateTime.now().toDate());
        if (itemKillSuccessMapper.countByKillUserId(itemKill.getId(),userId) <= 0){
            int res=itemKillSuccessMapper.insertSelective(entity);
            if(res>0){
                //处理抢购成功后的流程
                //这里的业务可以自己加
            }
        }
    }


对于订单抢购的数据库操作在ItemKillSuccessMapper 中进行,在mapper文件下编写ItemKillSuccessMapper :主要的操作有根据用户ID查询订单以及订单抢购成功后插入item_kill_success表


@Mapper
public interface ItemKillSuccessMapper {
    @Select("select count(1) from item_kill_success where user_id=#{userId} and kill_id=#{killId} and status in (0)")
    int countByKillUserId(@Param("killId") Integer killId, @Param("userId") Integer userId);
    @Insert("insert into item_kill_success(code,item_id,kill_id,user_id,status,create_time) values(#{code},#{itemId},#{killId},#{userId},#{status},#{createTime})")
    int insertSelective(ItemKillSuccess entity);
}


在ItemKillMapper中增加一条更新数据的代码,用来处理抢购成功后更新余量


@Update("update item_kill set total=total-1 where id=#{killId}")
int updateKillItem(Integer killId);


四、效果展示

运行项目,进入首页https://link.juejin.cn/?target=http%3A%2F%2Flocalhost%3A8080%2Fitem


点击详情:




点击抢购:


输出购买成功,数据库中生成一条信息


当再次购买时,显示已经抢购:


到当前功能的代码都放到https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FOliverLiy%2FSecondKill%2Ftree%2Fversion3.0


我搭建了一个微信公众号《Java鱼仔》,如果你对本项目有任何疑问,欢迎在公众号中联系我,我会尽自己所能为大家解答。

相关文章
|
10天前
|
消息中间件 缓存 Java
手写模拟Spring Boot启动过程功能
【11月更文挑战第19天】Spring Boot自推出以来,因其简化了Spring应用的初始搭建和开发过程,迅速成为Java企业级应用开发的首选框架之一。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,帮助读者深入理解其工作机制。
24 3
|
10天前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
29 0
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
48 4
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
155 1
|
1月前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
38 0
|
24天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
97 62
|
10天前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
垃圾识别分类系统。本系统采用Python作为主要编程语言,通过收集了5种常见的垃圾数据集('塑料', '玻璃', '纸张', '纸板', '金属'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对图像数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。然后使用Django搭建Web网页端可视化操作界面,实现用户在网页端上传一张垃圾图片识别其名称。
39 0
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
|
10天前
|
机器学习/深度学习 人工智能 算法
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
蔬菜识别系统,本系统使用Python作为主要编程语言,通过收集了8种常见的蔬菜图像数据集('土豆', '大白菜', '大葱', '莲藕', '菠菜', '西红柿', '韭菜', '黄瓜'),然后基于TensorFlow搭建卷积神经网络算法模型,通过多轮迭代训练最后得到一个识别精度较高的模型文件。在使用Django开发web网页端操作界面,实现用户上传一张蔬菜图片识别其名称。
48 0
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
|
20天前
|
前端开发 Java easyexcel
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
60 8
|
16天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
35 3
下一篇
无影云桌面