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

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

一、公共状态类封装

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

package com.sdxb.secondkill.response;
import com.sdxb.secondkill.enums.StatusCode;

public class BaseResponse {
    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秒

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

三、抢购处理逻辑编写

抢购逻辑中需要编写两项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);

四、效果展示

运行项目,进入首页http://localhost:8080/item

点击详情:

点击抢购:

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

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

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

到当前功能的代码都放到https://github.com/OliverLiy/SecondKill/tree/version3.0

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

你可能感兴趣的:(《一起实战吧!》系列,java,实战,高并发,面试,redis)