基于SpringBoot的秒杀demo

1环境搭建 pox.xml

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--mybatisplus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </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>
        </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>
 <dependencies>

application.yml

spring:
  # thymeleaf配置
 thymeleaf:
    # 关闭缓存
   cache: false
  # 数据源配置
 datasource:
   driver-class-name: com.mysql.cj.jdbc.Driver
   url: jdbc:mysql://localhost:3306/seckill?
useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
   username: root
   password: root
   hikari:
      # 连接池名
     pool-name: DateHikariCP
      # 最小空闲连接数
     minimum-idle: 5
      # 空闲连接存活最大时间,默认600000(10分钟)
     idle-timeout: 180000
      # 最大连接数,默认10
     maximum-pool-size: 10
      # 从连接池返回的连接的自动提交
     auto-commit: true
      # 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
     max-lifetime: 1800000
      # 连接超时时间,默认30000(30秒)
     connection-timeout: 30000
      # 测试连接是否可用的查询语句
     connection-test-query: SELECT 1
# Mybatis-plus配置
mybatis-plus:
  #配置Mapper映射文件
 mapper-locations: classpath*:/mapper/*Mapper.xml
  # 配置MyBatis数据返回类型别名(默认别名是类名)
 type-aliases-package: com.xxxx.seckill.pojo
## Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
 level:
   com.xxxx.seckill.mapper: debug

1.1通用公共结果返回对象

RespBeanEnum.java
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
	//通用
	SUCCESS(200, "SUCCESS"),
	ERROR(500, "服务端异常"),
	//登录模块5002xx
	LOGIN_ERROR(500210, "用户名或密码不正确"),
	MOBILE_ERROR(500211, "手机号码格式不正确"),
	BIND_ERROR(500212, "参数校验异常"),
	MOBILE_NOT_EXIST(500213, "手机号码不存在"),
	PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),
	SESSION_ERROR(500215, "用户不存在"),
	//秒杀模块5005xx
	EMPTY_STOCK(500500, "库存不足"),
	REPEATE_ERROR(500501, "该商品每人限购一件"),
	REQUEST_ILLEGAL(500502, "请求非法,请重新尝试"),
	ERROR_CAPTCHA(500503, "验证码错误,请重新输入"),
	ACCESS_LIMIT_REAHCED(500504, "访问过于频繁,请稍后再试"),
	//订单模块5003xx
	ORDER_NOT_EXIST(500300, "订单信息不存在"),
	;
	private final Integer code;
	private final String message;
}
RespBean.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {

	private long code;
	private String message;
	private Object obj;

	/**
	 * 功能描述: 成功返回结果
	 *
	 */
	public static RespBean success(){
		return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
	}

	/**
	 * 功能描述: 成功返回结果
	 *
	 * @param:
	 * @return:
	 *
	 */
	public static RespBean success(Object obj){
		return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
	}


	/**
	 * 功能描述: 失败返回结果
	 *
	 * @param:
	 * @return:
	 *
	 */
	public static RespBean error(RespBeanEnum respBeanEnum){
		return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
	}


	/**
	 * 功能描述: 失败返回结果
	 *
	 * @param:
	 * @return:

	 */
	public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
		return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
	}

}

1.2总体结构图

 2 分布式会话

2.1 实现登录功能

两次明文加密

客户端:PASS=MD5(明文+固定Salt)

服务端:PASS=MD5(用户属于+随机Salt)

导入MD5依赖

<!-- md5 依赖 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>

MD5工具类

MD5Util.java
@Component
public class MD5Util {

	public static String md5(String src){
		return DigestUtils.md5Hex(src);
	}

	private static final String salt="1a2b3c4d";


	public static String inputPassToFromPass(String inputPass){
		String str = "" +salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
		return md5(str);
	}

	public static String formPassToDBPass(String formPass,String salt){
		String str = "" +salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
		return md5(str);
	}

	public static String inputPassToDBPass(String inputPass,String salt){
		String fromPass = inputPassToFromPass(inputPass);
		String dbPass = formPassToDBPass(fromPass, salt);
		return dbPass;
	}


	public static void main(String[] args) {
		// d3b1294a61a07da9b49b6e22b2cbd7f9
		System.out.println(inputPassToFromPass("123456"));
		System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
		System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
	}

}

2.2 登录功能实现

逆向工程 生成实体类和mapper、service、controller、xml(参考Mybatis-plus)

网站链接:代码生成器(旧) | MyBatis-Plus

ValidatorUtil.Class 自定义手机号格式校验
public class ValidatorUtil {

	private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");

	public static boolean isMobile(String mobile){
		if (StringUtils.isEmpty(mobile)){
			return false;
		}
		Matcher matcher = mobile_pattern.matcher(mobile);
		return matcher.matches();
	}

}
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
  @Autowired
  private IUserService userService;
  /**
   * 跳转登录页
   *
   * @return
   */
  @RequestMapping("/toLogin")
  public String toLogin() {
     return "login";
  }
  /**
   * 登录
   * @return
   */
  @RequestMapping("/doLogin")
  @ResponseBody
  public RespBean doLogin(LoginVo loginVo) {
     log.info(loginVo.toString());
     return userService.login(loginVo);
  }
}

2.2.1登录接口实现

VO实体类

LoginVo.Class
@Data
public class LoginVo {
    @NotNull
    @IsMobile
    private String mobile;
    @NotNull
    @Length(min = 32)
    private String password;
}
UserServiceImpl
 @Override
   public RespBean login(LoginVo loginVo) {
      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();
      if (StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      if (!ValidatorUtil.isMobile(mobile)){
         return RespBean.error(RespBeanEnum.MOBILE_ERROR);
     }
      //根据手机号获取用户
      User user = userMapper.selectById(mobile);
      if (null==user){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      //校验密码
      if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      return RespBean.success();
   }

login.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
    <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- md5.js -->
    <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto">

    <h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>

    <div class="form-group">
        <div class="row">
            <label class="form-label col-md-4">请输入手机号码</label>
            <div class="col-md-5">
                <input id="mobile" name="mobile" class="form-control" type="text" placeholder="手机号码" required="true"
                       minlength="11" maxlength="11"/>
            </div>
            <div class="col-md-1">
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="row">
            <label class="form-label col-md-4">请输入密码</label>
            <div class="col-md-5">
                <input id="password" name="password" class="form-control" type="password" placeholder="密码"
                       required="true" minlength="6" maxlength="16"/>
            </div>
        </div>
    </div>

    <div class="row">
        <div class="col-md-5">
            <button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
        </div>
        <div class="col-md-5">
            <button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
        </div>
    </div>
</form>
</body>
<script>
    function login() {
        $("#loginForm").validate({
            submitHandler: function (form) {
                doLogin();
            }
        });
    }

    function doLogin() {
        g_showLoading();

        var inputPass = $("#password").val();
        var salt = g_passsword_salt;
        var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
        var password = md5(str);

        $.ajax({
            url: "/login/doLogin",
            type: "POST",
            data: {
                mobile: $("#mobile").val(),
                password: password
            },
            success: function (data) {
                layer.closeAll();
                if (data.code == 200) {
                    layer.msg("成功");
                    window.location.href="/goods/toList";
                } else {
                    layer.msg(data.message);
                }
            },
            error: function () {
                layer.closeAll();
            }
        });
    }
</script>
</html>

2.3参数校验

使用validation简化我们的代码

pom.xml

<!-- validation组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
自定义手机号码验证规则
IsMobileValidator.Class

*/
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {
   private boolean required = false;
   @Override
   public void initialize(IsMobile constraintAnnotation) {
      required = constraintAnnotation.required();
   }
   @Override
   public boolean isValid(String value, ConstraintValidatorContext context) {
      if (required){
         return ValidatorUtil.isMobile(value);
     }else {
         if (StringUtils.isEmpty(value)){
            return true;
         }else {
            return ValidatorUtil.isMobile(value);
         }
     }
   }
}

自定义注解

/**
* 验证手机号
*
* @author zhoubin
* @since 1.0.0
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {
   boolean required() default true;
   String message() default "手机号码格式错误";
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}

2.4 异常处理

异常处理主要分为编译时异常和运行时异常RuntimeException,前置时通过捕获异常获取到异常信息,后者是通过代码规范来减少运行时异常的发生,Springboot的全局异常处理有两种 

使用 @ControllerAdvice @ExceptionHandler 注解。
使用 ErrorController 来实现
定义全局异常处理类
GlobalException 
/**
* 全局异常
*
* @author zhoubin
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
   private RespBeanEnum respBeanEnum; }
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
   @ExceptionHandler(Exception.class)
   public RespBean ExceptionHandler(Exception e) {
      if (e instanceof GlobalException) {
         GlobalException ex = (GlobalException) e;
         return RespBean.error(ex.getRespBeanEnum());
     } else if (e instanceof BindException) {
         BindException ex = (BindException) e;
         RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
         respBean.setMessage("参数校验异常:" +
ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
         return respBean;
     }
      return RespBean.error(RespBeanEnum.ERROR);
   }
}

2.5 优化登录功能

UserArgumentResolver 判断用户是否登录 拦截器完成
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private IUserService service;
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> clazz = methodParameter.getParameterType();
        return clazz==User.class ;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = nativeWebRequest.getNativeRequest(HttpServletResponse.class);
        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        if (StringUtils.isEmpty(ticket)){
            return null;
        }
        return service.getUserByCookie(ticket,request,response);
    }
}
WebConfig 配置类
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserArgumentResolver userArgumentResolver;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }
}

秒杀功能

通过Mybatis-Plus自动生成商品对象,秒杀商品对象,商品订单,秒杀商品订单

3.1商品列表页

GoodsMapper.java
/**
* <p>
* Mapper 接口
* </p>
*
* @author zhoubin
* @since 1.0.0
*/
public interface GoodsMapper extends BaseMapper<Goods> {
   /**
    * 获取商品列表
    * @return
    */
   List<GoodsVo> findGoodsVo();
}
GoodsMapper.xml
 <select id="findGoodsVo" resultType="com.yrh.seckill.vo.GoodsVo">
    SELECT *
    FROM t_goods g
    LEFT JOIN t_seckill_goods sg
    ON g.id=sg.goods_id
    </select>
GoodsController 商品控制层
@Controller
@RequestMapping("/goods")
public class GoodsController {
   @Autowired
   private IGoodsService goodsService;
   /**
    * 跳转商品列表页
    *
    * @return
    */
   @RequestMapping("/toList")
   public String toLogin(Model model, User user) {
      model.addAttribute("user", user);
      model.addAttribute("goodsList", goodsService.findGoodsVo());
      return "goodsList";
   }
}

goodsList.html  商品信息

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品列表</title>
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-heading">秒杀商品列表</div>
    <table class="table" id="goodslist">
        <tr>
            <td>商品名称</td>
            <td>商品图片</td>
            <td>商品原价</td>
            <td>秒杀价</td>
            <td>库存数量</td>
            <td>详情</td>
        </tr>
        <tr th:each="goods,goodsStat : ${goodsList}">
            <td th:text="${goods.goodsName}"></td>
            <td><img th:src="@{${goods.goodsImg}}" width="100" height="100"/></td>
            <td th:text="${goods.goodsPrice}"></td>
            <td th:text="${goods.seckillPrice}"></td>
            <td th:text="${goods.stockCount}"></td>
            <td><a th:href="'/goodsDetail.htm?goodsId='+${goods.id}">详情</a></td>
        </tr>
    </table>
</div>
</body>
</html>

效果图

 3.2 详情页面

GoodsMapper
/**
* 根据商品id获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);
GoodsMapper.xml
<select id="findGoodsVoGoodsId" resultType="com.yrh.seckill.vo.GoodsVo">
      SELECT *
    FROM t_goods g
    LEFT JOIN t_seckill_goods sg
    ON g.id=sg.goods_id
    where g.id=#{goodsId}
    </select>
GoodsController
根据商品id跳转到商品详情
/**
* 跳转商品详情页
*
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping("/toDetail/{goodsId}")
public String toDetail(Model model, User user, @PathVariable Long goodsId) {
   model.addAttribute("user", user);
   GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
   model.addAttribute("goods", goods);
   Date startDate = goods.getStartDate();
   Date endDate = goods.getEndDate();
   Date nowDate = new Date();
   //秒杀状态
   int secKillStatus = 0;
   //剩余开始时间
   int remainSeconds = 0;
   //秒杀还未开始
   if (nowDate.before(startDate)) {
      remainSeconds = (int) ((startDate.getTime()-nowDate.getTime())/1000);
   // 秒杀已结束
   } else if (nowDate.after(endDate)) {
      secKillStatus = 2;
      remainSeconds = -1;
   // 秒杀中
   } else {
      secKillStatus = 1;
      remainSeconds = 0;
   }
   model.addAttribute("secKillStatus",secKillStatus);
   model.addAttribute("remainSeconds",remainSeconds);
   return "goodsDetail"; }

详情页面

goodsDetail.html
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品详情</title>
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-heading">秒杀商品详情</div>
    <div class="panel-body">
        <span th:if="${user eq null}"> 您还没有登录,请登陆后再操作<br/></span>
        <span>没有收货地址的提示。。。</span>
    </div>
    <table class="table" id="goods">
        <tr>
            <td>商品名称</td>
            <td colspan="3" th:text="${goods.goodsName}"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200"/></td>
        </tr>
        <tr>
            <td>秒杀开始时间</td>
            <td th:text="${#dates.format(goods.startDate,'yyyy-MM-dd HH:mm:ss')}"></td>
            <td id="seckillTip">
                <input type="hidden" id="remainSeconds" th:value="${remainSeconds}">
                <span th:if="${secKillStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒
                </span>
                <span th:if="${secKillStatus eq 1}">秒杀进行中</span>
                <span th:if="${secKillStatus eq 2}">秒杀已结束</span>
            </td>
            <td>
                <form id="secKillForm" method="post" action="/secKill/doSecKill">
                    <input type="hidden" name="goodsId" th:value="${goods.id}">
                    <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
                </form>
            </td>
        </tr>
        <tr>
            <td>商品原价</td>
            <td colspan="3" th:text="${goods.goodsPrice}"></td>
        </tr>
        <tr>
            <td>秒杀价</td>
            <td colspan="3" th:text="${goods.seckillPrice}"></td>
        </tr>
        <tr>
            <td>库存数量</td>
            <td colspan="3" th:text="${goods.stockCount}"></td>
        </tr>
    </table>
</div>
</body>
<script>
    $(function () {
        countDown();
    });
    function countDown() {
        var remainSeconds = $("#remainSeconds").val();
        var timeout;
        //秒杀还未开始
        if (remainSeconds > 0) {
            $("#buyButton").attr("disabled", true);
            timeout = setTimeout(function () {
                $("#countDown").text(remainSeconds - 1);
                $("#remainSeconds").val(remainSeconds - 1);
                countDown();
            }, 1000);
            // 秒杀进行中
        } else if (remainSeconds == 0) {
            if (timeout) {
                clearTimeout(timeout);
            }
            $("#buyButton").attr("disabled", false);
            $("#seckillTip").html("秒杀进行中")
        } else {
            $("#seckillTip").html("秒杀已经结束");
            $("#buyButton").attr("disabled", true);
        }
    };


</script>
</html>

效果图 秒杀未开始

秒杀进行中

秒杀结束

 

 

 3.3 秒杀功能实现

IOrderService.java

   /**
    * 秒杀
    * @param user
    * @param goods
    * @return
    */
   Order seckill(User user, GoodsVo goods);

订单接口实现类

OrderServiceImpl
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
IOrderService {
   @Autowired
   private ISeckillGoodsService seckillGoodsService;
   @Autowired
   private IGoodsService goodsService;
   @Autowired
   private OrderMapper orderMapper;
   @Autowired
   private ISeckillOrderService seckillOrderService;
   /**
    * 秒杀
    * @param user
    * @param goods
    * @return
    */
   @Override
   @Transactional
   public Order seckill(User user, GoodsVo goods) {
      //秒杀商品表减库存
      SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",
            goods.getId()));
      seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
      seckillGoodsService.updateById(seckillGoods);
      //生成订单
      Order order = new Order();
      order.setUserId(user.getId());
      order.setGoodsId(goods.getId());
      order.setDeliveryAddrId(0L);
      order.setGoodsName(goods.getGoodsName());
      order.setGoodsCount(1);
      order.setGoodsPrice(seckillGoods.getSeckillPrice());
      order.setOrderChannel(1);
      order.setStatus(0);
      order.setCreateDate(new Date());
      orderMapper.insert(order);
      //生成秒杀订单
      SeckillOrder seckillOrder = new SeckillOrder();
      seckillOrder.setOrderId(order.getId());
      seckillOrder.setUserId(user.getId());
      seckillOrder.setGoodsId(goods.getId());
      seckillOrderService.save(seckillOrder);
      return order;
   }
}
SeckillController 秒杀控制层
思路:判断用户是否登录
           判断商品是否还有库存
           判断改商品用户是否重复购买
@Controller
@RequestMapping("/seckill")
public class SeckillController {
   @Autowired
   private IGoodsService goodsService;
   @Autowired
   private ISeckillOrderService seckillOrderService;
   @Autowired
   private IOrderService orderService;
   @RequestMapping("/doSeckill")
   public String doSeckill(Model model, User user, Long goodsId) {
      if (user == null) {
         return "login";
     }
      model.addAttribute("user", user);
      GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount() < 1) {
         model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
         return "seckillFail";
     }
      //判断是否重复抢购
      SeckillOrder seckillOrder = seckillOrderService.getOne(new
QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq(
            "goods_id",
            goodsId));
      if (seckillOrder != null) {
         model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
         return "seckillFail";
     }
      Order order = orderService.seckill(user, goods);
      model.addAttribute("order",order);
      model.addAttribute("goods",goods);
      return "orderDetail";
   }
}
订单详情页
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品详情</title>
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-heading">秒杀订单详情</div>
    <table class="table" id="order">   
        <tr>           
            <td>商品名称</td>   
            <td th:text="${goods.goodsName}" colspan="3"></td>       
        </tr>       
        <tr>           
            <td>商品图片</td>           
            <td colspan="2"><img th:src="@{${goods.goodsImg}}" width="200"
                                 height="200"/></td>       
        </tr>       
        <tr>           
            <td>订单价格</td>           
            <td colspan="2" th:text="${order.goodsPrice}"></td>       
        </tr>       
        <tr>           
            <td>下单时间</td>           
            <td th:text="${#dates.format(order.createDate, 'yyyy-MM-dd HH:mm:ss')}" colspan="2"></td>       
        </tr>       
        <tr>           
            <td>订单状态</td>         
            <td>
                  <span th:if="${order.status eq 0}">未支付</span>
                  <span th:if="${order.status eq 1}">待发货</span>
                  <span th:if="${order.status eq 2}">已发货</span>
                  <span th:if="${order.status eq 3}">已收货</span>
                  <span th:if="${order.status eq 4}">已退款</span>
                  <span th:if="${order.status eq 5}">已完成</span>
                           
            </td>           
            <td>               
                <button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付
                </button>           
            </td>       
        </tr>     
        <tr>           
            <td>收货人</td>           
            <td colspan="2">XXX 18012345678</td>
        </tr>
          <tr>
         <td>收货地址</td>
                   <td colspan="2">上海市浦东区世纪大道</td>
               </tr>
           </table>
</div>
</body>
</html>

4 页面优化

4.1 页面缓存

/**
     * @author: yrh
     * @Description: 商品列表
     *  * window优化前 吞吐量:452.55558136798504
     *  页面静态化
     *  * window优化后 吞吐量:950.8475220913575
    * @return: null
     */
    @RequestMapping(value = "/toList",produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toList(Model model,User user,
                         HttpServletRequest request,
                         HttpServletResponse response){
//        if (StringUtils.isEmpty(ticket)){
//            return "login";
//        }
//        User user = service.getUserByCookie(ticket, request, response);
//        if (null==user){
//            return "login";
//        }
        //获取缓存
        ValueOperations operations = redisTemplate.opsForValue();
        String html = (String) operations.get("goodsList");
        if (!StringUtils.isEmpty(html)){
            return html;
        }
        model.addAttribute("user",user);
        model.addAttribute("goodsList",goodsService.findGoodsVo());
        //缓存为空

        WebContext context=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
        html= thymeleafViewResolver.getTemplateEngine().process("goodsList",context);
        if (!StringUtils.isEmpty(html)){
            operations.set("goodsList",html,60, TimeUnit.SECONDS);
        }
        return html;
    }

详情页面缓存

 /**
     * @author: yrh
     * @Description: 商品详情
     *
    * @return: null
     */
    @RequestMapping(value = "/toDetail2/{goodsId}",produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toDetail2(Model model,User user,@PathVariable Long goodsId,
                           HttpServletRequest request,
                           HttpServletResponse response){
        ValueOperations operations = redisTemplate.opsForValue();
        //是否在缓存当中
        String html = (String) operations.get("goodsDetail:" + goodsId);
        if (!StringUtils.isEmpty(html)){
            return html;
        }
        model.addAttribute("user",user);
        GoodsVo goodsVo = goodsService.findGoodsVoGoodsId(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();
        //秒杀状态
        int secKillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;
        //秒杀还未开始
        if (nowDate.before(startDate)) {
            remainSeconds = ((int) ((startDate.getTime() - nowDate.getTime()) / 1000));
        } else if (nowDate.after(endDate)) {
            //	秒杀已结束
            secKillStatus = 2;
            remainSeconds = -1;
        } else {
            //秒杀中
            secKillStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("goods",goodsVo);
        model.addAttribute("secKillStatus",secKillStatus);
        model.addAttribute("remainSeconds",remainSeconds);
        WebContext context=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
        html= thymeleafViewResolver.getTemplateEngine().process("goodsDetail",context);
         if (!StringUtils.isEmpty(html)){
             operations.set("goodsDetail:"+ goodsId,html,60,TimeUnit.SECONDS);
         }
        return html;
    }

4.2 解决库存超卖

减库存时判断库存是否足够
OrderServiceImpl.java
//秒杀商品表减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",
      goods.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", 
seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count", 
0));
// seckillGoodsService.updateById(seckillGoods);
解决同一用户同时秒杀多件商品。
可以通过数据库建立唯一索引避免

商品表创建唯一索引 

将秒杀订单存放在redis中,方便查询是否重复
OrderServiceImpl.java
 @Override
    @Transactional
    public Order seckill(User user, GoodsVo goods) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //秒杀商品表减库存
        SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id", goods.getId()));
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
//        seckillGoodsService.updateById(seckillGoods);
        //修改后的秒杀商品表减库存
        boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
                .setSql("stock_count=" + "stock_count-1").eq("goods_id", goods.getId()).gt("stock_count", 0));
        if (seckillGoods.getStockCount() < 1) {
            //判断是否还有库存
            valueOperations.set("isStockEmpty:" + goods.getId(), "0");
            return null;
        }
        //生成订单
        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goods.getId());
        order.setDeliveryAddrId(0L);
        order.setGoodsName(goods.getGoodsName());
        order.setGoodsCount(1);
        order.setGoodsPrice(seckillGoods.getSeckillPrice());
        order.setOrderChannel(1);
        order.setStatus(0);
        order.setCreateDate(new Date());
        orderMapper.insert(order);
        //生成秒杀订单
        //生成秒杀订单
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setOrderId(order.getId());
        seckillOrder.setUserId(user.getId());
        seckillOrder.setGoodsId(goods.getId());
        seckillOrderService.save(seckillOrder);
        //存入缓存
        redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(), JsonUtil.object2JsonStr(seckillOrder));
        return order;

    }
seckillController.java
@RequestMapping("/seckill")
public class SeckillController {
   @Autowired
   private IGoodsService goodsService;
   @Autowired
   private ISeckillOrderService seckillOrderService;
   @Autowired
   private IOrderService orderService;
   @Autowired
   private RedisTemplate redisTemplate;
   @RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
   @ResponseBody
   public RespBean doSeckill(User user, Long goodsId) {
      if (user == null) {
         return RespBean.error(RespBeanEnum.SESSION_ERROR);
     }
      GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount() < 1) {
         return RespBean.error(RespBeanEnum.EMPTY_STOCK);
     }
      //判断是否重复抢购
      // SeckillOrder seckillOrder = seckillOrderService.getOne(new 
QueryWrapper<SeckillOrder>().eq("user_id",
      //       user.getId()).eq(
      //       "goods_id",
      //       goodsId));
      String seckillOrderJson = (String) 
redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
         return RespBean.error(RespBeanEnum.REPEATE_ERROR);
     }
      Order order = orderService.seckill(user, goods);
      if (null != order) {
         return RespBean.success(order);
     }
      return RespBean.error(RespBeanEnum.ERROR);
   }
}

5. 接口优化

思路:减少数据库的访问
1,系统初始化的时候把商品库存压入到缓存当中
2,收到请求redis预减库存,库存不足时直接返回,否则进入下一步
3,请求入队,立刻返回排队中
4,请求出队,生成订单和秒杀订单
5,客服端轮询,是否秒杀成功

5.1 Redis操作库存

实现InitializingBean接口 重新初始方法 向redis中压入库存

public class SecKillController implements InitializingBean 


/**
     * @author: yrh
     * @Description: 系统初始化,把商品库存数量加载到Redis
     * throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        List<GoodsVo> list = goodsService.findGoodsVo();
        //压入缓存
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        list.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            EmptyStockMap.put(goodsVo.getId(), false);
        });

    }

5.2 RabbitMQ秒杀

SeckillMessage.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeckillMessage {
    private User user;
    private Long goodsId;
}

       配置RabbitMQ队列和交换机规则

RabbitTopicConfig.class
@Configuration
public class RabbitTopicConfig {
    private static final String QUEUE="seckillQueue";
    private static final String EXCHANGE="seckillExchange";
    private static final String ROUTINGKEY="seckill.#";
    //秒杀队列
    @Bean
    public Queue queue(){
        return new Queue(QUEUE);
    }
    //声明交换机
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(EXCHANGE);
    }
    //绑定交换机和队列
    @Bean
    public Binding binding(){
      return BindingBuilder.bind(queue()).to(topicExchange()).with(ROUTINGKEY);
    }
}

信息发送者

MQSender.class
@Service
@Slf4j
public class MQSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendSecKillMessage(Object msg){
        log.info("发送消息:"+msg);
        rabbitTemplate.convertAndSend("seckillExchange","seckill.msg",msg);
    }
}

消息消费者 用来异步下单

MQReceiver.class
@Service
@Slf4j
public class MQReceiver {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private IOrderService orderService;

    //    @RabbitListener(queues = "queue")
//    public void receive(Object msg){
//        log.info("接收消息:"+msg);
//    }
//    @RabbitListener(queues = "queue_topic03")
//    public void receive01(Object msg){
//        log.info("接收消息:"+msg);
//    }
//    @RabbitListener(queues = "queue_topic04")
//    public void receive02(Object msg){
//        log.info("接收消息:"+msg);
//    }
    @RabbitListener(queues = "seckillQueue")
    public void receive(String msg) {
        log.info("接收消息:" + msg);
        //json转对象
        SeckillMessage message = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);
        User user = message.getUser();
        Long goodsId = message.getGoodsId();
        GoodsVo goodsVo = goodsService.findGoodsVoGoodsId(goodsId);
        //判断库存
        if (goodsVo.getStockCount() < 1) {
            return;
        }
        //判断是否是重复抢购
        String seckillOrderJson = (String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (!StringUtils.isEmpty(seckillOrderJson)){
            return;
        }
        orderService.seckill(user,goodsVo);
    }
}

修改秒杀接口 下单操作先进入队列

@Controller
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {
 @Autowired
 private IGoodsService goodsService;
 @Autowired
 private ISeckillOrderService seckillOrderService;
 @Autowired
 private IOrderService orderService;
 @Autowired
 private RedisTemplate redisTemplate;
 @Autowired
 private MQSender mqSender;
 private Map<Long, Boolean> EmptyStockMap = new HashMap<>();
 * @param user
 * @param goodsId
 * @return
 */
 @RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
 @ResponseBody
 public RespBean doSeckill(User user, Long goodsId) {
 if (user == null) {
 return RespBean.error(RespBeanEnum.SESSION_ERROR);
 }
 GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
 //判断库存
 if (goods.getStockCount() < 1) {
 return RespBean.error(RespBeanEnum.EMPTY_STOCK);
 }

 String seckillOrderJson = (String) 
redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
 if (!StringUtils.isEmpty(seckillOrderJson)) {
 return RespBean.error(RespBeanEnum.REPEATE_ERROR);
 }
 Order order = orderService.seckill(user, goods);
 if (null != order) {
 return RespBean.success(order);
 }*/
 ValueOperations valueOperations = redisTemplate.opsForValue();
 //判断是否重复抢购
 String seckillOrderJson = (String) valueOperations.get("order:" +
user.getId() + ":" + goodsId);
 if (!StringUtils.isEmpty(seckillOrderJson)) {
 return RespBean.error(RespBeanEnum.REPEATE_ERROR);
 }
 //内存标记,减少Redis访问
 if (EmptyStockMap.get(goodsId)) {
 return RespBean.error(RespBeanEnum.EMPTY_STOCK);
 }
 //预减库存
 Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
 if (stock < 0) {
 EmptyStockMap.put(goodsId,true);
 valueOperations.increment("seckillGoods:" + goodsId);
 return RespBean.error(RespBeanEnum.EMPTY_STOCK);
 }
 // 请求入队,立即返回排队中
 SeckillMessage message = new SeckillMessage(user, goodsId);
 mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));
 return RespBean.success(0);
 }

5.3 客户端轮询秒杀结果

SeckillController.class
 /**
     * @author: yrh
     * @Description: 轮询秒杀接口
     * @return: orderId :成功,-1失败,0排队中
     */
    @RequestMapping(value = "/result", method = RequestMethod.GET)
    @ResponseBody
    public RespBean result(User user, Long goodsId) {
        if (null == user) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        Long orderId = seckillOrderService.getResult(user, goodsId);
        return RespBean.success(orderId);
    }
ISeckillOrderService.class
Long getResult(User user, Long goodsId);

接口实现类

SeckillOrderServiceImpl.class
 @Override
    public Long getResult(User user, Long goodsId) {
        SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId())
                .eq("goods_id", goodsId));
        if (null != seckillOrder) {
            return seckillOrder.getOrderId();
        } else {
           if ( redisTemplate.hasKey("isStockEmpty:" + goodsId)){
               return -1L;
           }else {
               return 0L;
           }
        }
    }

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值