资讯专栏INFORMATION COLUMN

java并发编程学习19--基于springboot的秒杀系统实现1--项目介绍

CollinPeng / 2389人阅读

摘要:当秒杀日期尚未达到会提示用户秒杀尚未开始当用户多次秒杀同一商品会提示用户重复秒杀当秒杀日期过期或者秒杀商品的库存为零会提示用户秒杀结束。

【秒杀系统业务分析

在秒杀系统当中有两个核心的表:秒杀商品(kill_product)与秒杀明细(kill_item),具体的逻辑是一个用户秒杀商品的库存减一,秒杀明细的记录增加一条。这两步作是处于同一事务之中。

当秒杀日期尚未达到会提示用户秒杀尚未开始;

当用户多次秒杀同一商品会提示用户重复秒杀;

当秒杀日期过期或者秒杀商品的库存为零会提示用户秒杀结束。

【秒杀项目结构

java目录下

web:controller以及rest接口

applicationService:所有的写操作业务逻辑接口

queryService:所有的读操作业务逻辑接口

dao:数据传输层包括:mysql以及redis

common:所有的常量以及枚举

aop:针对request进行拦截,在日志中打印每个接口耗时毫秒值

configuration:所有的配置信息

exception:所有的业务异常

dto:数据传输对象

resources目录下

static:存放静态资源:javascript,css,图片

template:H5模板,我们的项目采用的是Thymeleaf

application.properties:通用的配置信息

application-*.properties:根据环境不同而不同的配置信息,比如开发环境数据库地址

test目录下

单元测试代码

【Entity设计

秒杀商品实体:注意一下:product_id只是用于表示秒杀商品是属于哪一个实体商品,本项目不会用到该字段

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

/**
 * 秒杀产品实体类
 * @author ibm
 * @since 0
 * @date 2018/3/22
 */
@Entity
@Table(name = "kill_product")
@Data
public class KillProduct {

    /**
     * ID
     */
    @Id
    @Column(name = "id")
    private String id;
    /**
     * 产品ID
     */
    @Column(name = "product_id")
    private String productId;
    /**
     * 秒杀描述信息
     */
    @Column(name = "kill_description")
    private String killDescription;
    /**
     * 库存数量
     */
    @Column(name = "number")
    private String number;
    /**
     * 秒杀开始时间
     */
    @Column(name = "start_time")
    private Date startTime;
    /**
     * 秒杀结束时间
     */
    @Column(name = "end_time")
    private Date endTime;

}

秒杀明细实体:记录一次成功的秒杀,类上关于Procedure的注解是为了提供高并发调用存储过程支持而加入的。

import lombok.Data;

import javax.persistence.*;
import java.util.Date;

/**
 * 秒杀明细实体类
 * @author ibm
 * @since 0
 * @date 2018/3/22
 */
@Entity
@Table(name = "kill_item")
@NamedStoredProcedureQuery(name = "executeSeckill", procedureName = "execute_seckill", parameters = {
        @StoredProcedureParameter(mode = ParameterMode.IN, name = "v_id", type = String.class),
        @StoredProcedureParameter(mode = ParameterMode.IN, name = "v_kill_product_id", type = String.class),
        @StoredProcedureParameter(mode = ParameterMode.IN, name = "v_mobile", type = Long.class),
        @StoredProcedureParameter(mode = ParameterMode.IN, name = "v_kill_time", type = Date.class),
        @StoredProcedureParameter(mode = ParameterMode.OUT, name = "r_result", type = Integer.class) })
@Data
public class KillItem {

    /**
     * 记录ID
     */
    @Id
    @Column(name = "id")
    private String id;
    /**
     * 秒杀产品id
     */
    @Column(name = "kill_product_id")
    private String killProductId;
    /**
     * 用户手机号码
     */
    @Column(name = "mobile")
    private String mobile;
    /**
     * 秒杀成功时间
     */
    @Column(name = "kill_time")
    private Date killTime;
}
【JPA设计

秒杀商品的JPA的核心方法就是修改库存

import com.example.seckill.dao.entity.KillProduct;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

import java.util.Date;
import java.util.List;

/**
 * @author ibm
 * @since 0
 * @date 2018/3/22
 */
public interface KillProductJpaRepo extends JpaRepository{

    /**
     * 查看可以开始秒杀商品
     * @param now 开始时间点
     * @return 秒杀商品明细
     */
    List findAllByStartTimeAfter(Date now);

    /**
     * 减少库存,库存等于0就不再减少
     * @param id 秒杀商品id
     * @param time 执行秒杀的时间
     * @return 执行的行数
     */
    @Modifying
    @Query(value = "UPDATE kill_product SET number = number - 1 WHERE id = ?1 AND number >= 1 AND end_time > ?2",
    nativeQuery = true)
    int reduceNumber(String id,Date time);
}

秒杀明细的JPA核心就是增加一条成功秒杀的明细,这里还会提供一个针对存储过程调用的方法

import com.example.seckill.dao.entity.KillItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;

import java.util.Date;
import java.util.List;

/**
 * @author ibm
 * @since 0
 * @date 2018/3/22
 */
public interface KillItemJpaRepo extends JpaRepository {

    /**
     * 查看秒杀商品的秒杀记录
     * @param killProductId 秒杀商品Id
     * @return 秒杀记录详情
     */
    List findAllByKillProductIdOrderByKillTimeDesc(String killProductId);

    /**
     * 保存秒杀记录
     * @param id 预生成的主键
     * @param killProductId 秒杀商品id
     * @param mobile 执行秒杀用户手机号
     * @return 执行的行数
     */
    @Modifying
    @Query(value = "INSERT IGNORE INTO kill_item(id,kill_product_id,mobile) values(?1,?2,?3)",
            nativeQuery = true)
    int insertKillItem(String id,String killProductId,long mobile);


    @Procedure(procedureName = "execute_seckill")
    int executeProcedure(@Param("v_id")String killItemId,
                         @Param("v_kill_product_id")String killProductId,
                         @Param("v_mobile")long mobile,
                         @Param("v_kill_time")Date killTime);
}
【applicationService设计

applicationService会提供两个方法一个是将事务交个spring控制的方式,另个一个是将事务直接交给MySQL控制的,而高并发一个重要的优化点就是减少行级锁的持有时间,而有效的方式就是取消spring提供的声明式事务,将事务完全交个MySQL,这样网络延迟与GC的时间都可以得到节约。并且我们也需要在提供了秒杀地址的时候,返回一个md5的加密数据,保证秒杀不会被篡改数据。

import com.example.seckill.applicationService.ISecKillApplicationService;
import com.example.seckill.common.status.KillStatus;
import com.example.seckill.common.utils.IdUtil;
import com.example.seckill.common.utils.Md5Util;
import com.example.seckill.configuration.cache.RedisCacheName;
import com.example.seckill.dao.entity.KillItem;
import com.example.seckill.dao.repository.KillItemJpaRepo;
import com.example.seckill.dao.repository.KillProductJpaRepo;
import com.example.seckill.dto.Execution;
import com.example.seckill.exception.KillClosedException;
import com.example.seckill.exception.RepeatKillException;
import com.example.seckill.exception.SecKillException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.Date;


/**
 * @author ibm
 */
@CacheConfig(cacheNames = RedisCacheName.KILL_PRODUCT)
@Service
public class SecKillApplicationServiceImpl implements ISecKillApplicationService{

    @Autowired
    private KillProductJpaRepo killProductJpaRepo;

    @Autowired
    private KillItemJpaRepo killItemJpaRepo;

    @Override
    @CacheEvict(keyGenerator = "keyGenerator")
    @Transactional(rollbackFor = RuntimeException.class)
    public Execution executeSecKill(String killProductId, long mobile, String md5) throws SecKillException, RepeatKillException, KillClosedException {
        if(StringUtils.isEmpty(md5) || !md5.equals(Md5Util.getMd5(killProductId))){
            throw new SecKillException(KillStatus.REWRITE.getInfo());
        }
        //执行秒杀逻辑:减库存 + 插入秒杀明细
        try{
            Date now = new Date();
            int updateCount = killProductJpaRepo.reduceNumber(killProductId,now);
            if(updateCount <= 0){
                throw new KillClosedException(KillStatus.END.getInfo());
            }else {
                //记录秒杀明细
                String itemId = IdUtil.getObjectId();
                int insertCount = killItemJpaRepo.insertKillItem(itemId,killProductId,mobile);
                if(insertCount <= 0){
                    throw new RepeatKillException(KillStatus.REPEAT_KILL.getInfo());
                }else {
                    KillItem killItem = killItemJpaRepo.findById(itemId).get();
                    return new Execution(killProductId, KillStatus.SUCCESS,killItem);
                }
            }
        }catch (RepeatKillException e1){
            throw e1;
        }catch (KillClosedException e2){
            throw e2;
        }catch (Exception e){
            throw new SecKillException(KillStatus.INNER_ERROR.getInfo());
        }
    }

    @Override
    public Execution executeSecKillProcedure(String killProductId, long mobile, String md5){
        if(StringUtils.isEmpty(md5) || !md5.equals(Md5Util.getMd5(killProductId))){
            throw new SecKillException(KillStatus.REWRITE.getInfo());
        }
        String itemId = IdUtil.getObjectId();
        int reuslt = killItemJpaRepo.executeProcedure(itemId,killProductId,mobile,new Date());
        if(KillStatus.SUCCESS.getValue() == reuslt){
            KillItem killItem = killItemJpaRepo.findById(itemId).get();
            return new Execution(killProductId, KillStatus.SUCCESS,killItem);
        }else if(KillStatus.REPEAT_KILL.getValue() == reuslt){
            throw new RepeatKillException(KillStatus.REPEAT_KILL.getInfo());
        }else if(KillStatus.END.getValue() == reuslt){
            throw new KillClosedException(KillStatus.END.getInfo());
        }else {
            throw new SecKillException(KillStatus.INNER_ERROR.getInfo());
        }
    }
}
【rest设计

提供的接口:

秒杀列表,使用Thymeleaf模板返回

秒杀详情,使用Thymeleaf模板返回

获取秒杀地址与md5(Ajax),使用json返回

获取系统时间,使用json返回

执行秒杀(Ajax),使用json返回

import com.example.seckill.applicationService.ISecKillApplicationService;
import com.example.seckill.dao.entity.KillProduct;
import com.example.seckill.dto.Execution;
import com.example.seckill.dto.Exposer;
import com.example.seckill.exception.SecKillException;
import com.example.seckill.queryService.ISecKillQueryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/**
 * 秒杀相关web接口
 * @author ibm
 * @since 0
 * @date 2018/3/22
 */
@Controller
@RequestMapping("/secKill")
public class SecKillRest {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final ISecKillQueryService secKillQueryService;
    private final ISecKillApplicationService secKillApplicationService;
    
    @Autowired
    public SecKillRest(ISecKillQueryService secKillQueryService,ISecKillApplicationService secKillApplicationService){
        this.secKillQueryService = secKillQueryService;
        this.secKillApplicationService = secKillApplicationService;
    }

    /**
     * 秒杀列表页
     * @param model 封装返回对象使用
     * @return 列表页视图
     */
    @GetMapping("/list")
    public String getList(Model model){
        List list = secKillQueryService.getKillProductList();
        model.addAttribute("list",list);
        return "/list";
    }
    /**
     * 秒杀详情页
     * @param killProductId 秒杀商品Id
     * @param model 封装返回对象使用
     * @return 详情页视图
     */
    @GetMapping("/{killProductId}/detail")
    public String getDetail(@PathVariable("killProductId")String killProductId, Model model){
        if(StringUtils.isEmpty(killProductId)){
            return "redirect:/secKill/list";
        }
        Optional killProductOptional = secKillQueryService.getKillProductById(killProductId);
        if(!killProductOptional.isPresent()){
            return "forward:/secKill/list";
        }
        KillProduct killProduct = killProductOptional.get();
        model.addAttribute("killProduct",killProduct);
        return "detail";
    }
    /**
     * 查看秒杀商品是否暴露
     * @param killProductId 秒杀商品Id
     * @return 是否暴露
     */
    @PostMapping("/{killProductId}/expose")
    @ResponseBody
    public Exposer expose(@PathVariable("killProductId") String killProductId){
        return secKillQueryService.exportSecKillUrl(killProductId);
    }
    /**
     * 执行秒杀
     * @param killProductId 秒杀商品Id
     * @param md5 加密值
     * @param mobile 用户登陆手机号
     * @return 秒杀结果
     */
    @PostMapping("/{killProductId}/{md5}/execute")
    @ResponseBody
    public Execution execute(@PathVariable("killProductId") String killProductId,
                                               @PathVariable("md5")String md5,
                                               @CookieValue("killPhone") Long mobile){
        if(mobile == null){
            throw new SecKillException("用户未登录");
        }
        return secKillApplicationService.executeSecKillProcedure(killProductId,mobile,md5);
    }
    /**
     * 获取当前系统时间
     * @return
     */
    @GetMapping("/time/now")
    @ResponseBody
    public Long time(){
        return System.currentTimeMillis();
    }
}
【项目效果

秒杀列表

秒杀详情

秒杀成功

【项目地址

https://github.com/jipingongz...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69089.html

相关文章

  • java并发编程学习21--基于springboot秒杀系统实现3--存储过程

    摘要:但是经过测试自身的是次秒,是一个相当不错的数据,所以我们这里将事务直接交给,使用存储过程来降低行级锁的持有时间。存储过程代码使用存储过程之前必须保证数据库已经创建了存储过程。表示使用在存储过程中替代最后需要还原回来。 【什么是存储过程 所谓的存储过程是指:是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执...

    keithyau 评论0 收藏0
  • java并发编程学习20--基于springboot秒杀系统实现2--redis缓存

    摘要:在查询的服务方法上添加如下注解表明该方法的返回值需要缓存。当被缓存的数据发生改变,缓存需要被清理或者修改,这里使用如下注解清除指定的缓存。事务是一个原子操作,所有的缓存,消息,这种非强一致性要求的操作,都应该在事务成功提交后执行。 【为什么使用redis 性能极高,redis能读的速度是110000次/s,写的速度是81000次/s 丰富的数据类型,redis支持二进制案例的 Str...

    bovenson 评论0 收藏0

发表评论

0条评论

CollinPeng

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<