技术点介绍:
前端:Thymeleaf,Bootstrap,Jquerry
后端:SpringBoot,MybatisPlus,Lombok
中间件:RabbitMQ,Redis
秒杀方案:
分布式会话:用户登录,共享session
功能开发:商品列表,商品详情,秒杀,订单详情
系统压测:JMeter入门,yace
页面优化:缓存,静态化分离
服务优化:RabbitMQ消息队列,接口优化,分布式锁
安全优化:隐藏秒杀地址,算术验证码,接口限流。
秒杀介绍:
秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀对于我们系统而言是一个巨大的考验。秒杀主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。

秒杀的整体架构可以概括为“稳、准、快”几个关键字。稳要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提;“准”就是要求保证数据的一致性,秒杀几台东西就只能卖几台东西;“快”就是要求系统的性能要足够高。

从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求。

高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。对应的方案比如动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化.
一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知
高可用。 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。
项目搭建
选择Spring Initializr, sdk选择1.8,java版本选择8,添加Lombok,Spring Web,Thymeleaf依赖。

添加依赖:pom.xml

<?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>
<!--SpringBoot依赖--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.xxxx</groupId><artifactId>seckill-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>seckill-demo</name><description>Demo project for Spring Boot</description><properties><java.version>8</java.version></properties><dependencies><!--thymeleaf 组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!--web 组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mybatisplus依赖--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--test组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!-- md5 依赖 --><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId></dependency><!-- validation组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!--spring data redis 依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--commons-pool2 对象池依赖--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!-- AMQP依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><!-- 验证码依赖 --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

配置文件:application.yml

spring:#静态资源处理resources:#启动默认静态资源处理,默认启用add-mappings: truecache:cachecontrol:#缓存相应时间,单位秒max-age: 3600chain:#资源链启动缓存,默认启动cache: true#启用资源链,默认禁用enabled: true#启用压缩资源(gzip,brotli)解析,默认禁用compressed: true#启用h5应用缓存,默认禁用html-application-cache: truestatic-locations: classpath:/static/# thymelaef配置thymeleaf:# 关闭缓存cache: false# 数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.56.10:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: roothikari:#连接池名pool-name: DateHikariCP# 最小空闲连接出minimum-idle: 5# 空闲连接存活最大时间,默认600000(10分钟)idle-timeout: 600000#最大连接数,默认10maximum-pool-size: 10# 从连接池返回的连接自动提交auto-commit: true# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)max-lifetime: 1800000# 连接超时时间,默认30000(30秒)connection-timeout: 30000# 测试连接是否可用的查询语句connection-test-query: SELECT 1# redis配置redis:#服务器地址host: 192.168.56.10#端口port: 6379#数据库database: 0#超时时间timeout: 10000ms#密码lettuce:pool:#最大连接数,默认8max-active: 8#最大连接阻塞等待时间,默认-1max-wait: 10000ms#最大空闲连接,默认8max-idle: 200#最小空闲连接,默认0min-idle: 5# RabbitMQrabbitmq:# 服务器host: 192.168.56.10#用户名username: root#密码password: root# 虚拟主机virtual-host: /#端口port: 5672listener:simple:#消费者最小数量concurrency: 10#消费者最大数量max-concurrency: 10#限制消费者每次只处理一条消息,处理完再继续下一条消息prefetch: 1#启动时是否默认启动容器,默认trueauto-startup: true#被拒绝时重新进入队列default-requeue-rejected: truetemplate:retry:#发布重试,默认falseenabled: true#重试时间,默认1000msinitial-interval: 1000ms#重试最大次数,默认3次max-attempts: 3#重试最大间隔时间,默认10000msmax-interval: 10000ms#重试的间隔乘数。比如配2.0,第一次就等10s,第二次就等20s,第三次就等40smultiplier: 1#Mybatis-plus配置
mybatis-plus:# 配置Mapper.xml映射文件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

添加公共结果返回对象:

//公共返回对象枚举
public enum RespBeanEnum {//通用SUCCESS(200, "SUCCESS"),ERROR(500, "服务端异常"),//登录模块5002xxLOGIN_ERROR(500210, "用户名或密码不正确"),MOBILE_ERROR(500211, "手机号码格式不正确"),BIND_ERROR(500212, "参数校验异常"),MOBILE_NOT_EXIST(500213, "手机号码不存在"),PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),SESSION_ERROR(500215, "用户不存在"),//秒杀模块5005xxEMPTY_STOCK(500500, "库存不足"),REPEATE_ERROR(500501, "该商品每人限购一件"),REQUEST_ILLEGAL(500502, "请求非法,请重新尝试"),ERROR_CAPTCHA(500503, "验证码错误,请重新输入"),ACCESS_LIMIT_REAHCED(500504, "访问过于频繁,请稍后再试"),//订单模块5003xxORDER_NOT_EXIST(500300, "订单信息不存在"),;private final Integer code;private final String message;
}
//公共返回对象
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);}/*** 功能描述: 成功返回结果*/public static RespBean success(Object obj){return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);}/*** 功能描述: 失败返回结果*/public static RespBean error(RespBeanEnum respBeanEnum){return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);}/*** 功能描述: 失败返回结果*/public static RespBean error(RespBeanEnum respBeanEnum,Object obj){return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);}}

分布式会话
登录功能:两次MD5加密

客户端 → 服务端:MD5(用户输入 + salt)
服务端 → 数据库:MD5(服务端结果 + salt)

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) {// d3b1294a61a07da9b49b6e22b2cbd7f9System.out.println(inputPassToFromPass("123456"));System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));System.out.println(inputPassToDBPass("123456","1a2b3c4d"));}
}

通过逆向工程基于 t_user 表生产对应的POJO、Mapper、Service、ServiceImpl、Controller

等类,项目中使用了MybatisPlus,所以逆向工程也是用了MybatisPlus提供的AutoGenerator.

手机号码校验:

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();}
}

自定义注解,@IsMobile来判断是否为手机号

@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 { };
}
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {private boolean required = false;@Overridepublic void initialize(IsMobile constraintAnnotation) {required = constraintAnnotation.required();}@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {if (required){return ValidatorUtil.isMobile(value);}else {if (StringUtils.isEmpty(value)){return true;}else {return ValidatorUtil.isMobile(value);}}}
}

异常处理:

系统中异常包括:编译时异常和运行时异常 RuntimeException ,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管是dao层、service层还是controller层,都有可能抛出异常,在Springmvc中,能将所有类型的异常处理从各处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。SpringBoot全局异常处理方式主要两种:

使用 @ControllerAdvice 和 @ExceptionHandler 注解
使用 ErrorController类来实现

区别:

@ControllerAdvice 方式只能处理控制器抛出的异常。此时请求已经进入控制器中。
ErrorController类 方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误
如果应用中两者共同存在,则 @ControllerAdvice 方式处理控制器抛出的异常,ErrorController类 方式处理未进入控制器的异常。
@ControllerAdvice 方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常信息,自由度更大

//全局异常
public class GlobalException extends RuntimeException {private RespBeanEnum respBeanEnum;
}
//全局异常处理类
@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);}
}

每次用户登录,根据UUIDUtil工具类,生成随机字符,并作为userTicket的cookie值,之后可以通过cookie获取用户。

public class UUIDUtil {public static String uuid() {return UUID.randomUUID().toString().replace("-", "");}
}
{String ticket = UUIDUtil.uuid();request.getSession().setAttribute(ticket,user);CookieUtil.setCookie(request, response, "userTicket", ticket);
}
//根据cookie获取用户
public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {if (StringUtils.isEmpty(userTicket)) {return null;}User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);if (user != null) {CookieUtil.setCookie(request, response, "userTicket", userTicket);}return user;}

分布式session问题:
之前的代码在我们之后一台应用系统,所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题。

原因:由于 Nginx 使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。也就是说刚开始我们在 Tomcat1 登录之后,用户信息放在 Tomcat1 的 Session 里。过了一会,请求又被 Nginx 分发到了 Tomcat2 上,这时 Tomcat2 上 Session 里还没有用户信息,于是又要登录。

解决方案:

1.session复制
优点:无需修改代码,只需要修改Tomcat配置
缺点:Session同步传输占用内网带宽;多台Tomcat同步性能指数级下降;Session占用内存,无法有效水平扩展
2.前端存储
优点:不占用服务端内存
缺点:存在安全风险;数据大小受cookie限制;占用外网带宽
3.Session粘滞
优点:无需修改代码;服务端可以水平扩展
缺点:增加新机器,会重新Hash,导致重新登录;应用重启,需要重新登录
4.后端集中存储
优点:安全,容易水平扩展
缺点:增加复杂度,需要修改代码

综上,我们可以使用redis中间件作为后端存储,实现分布式session。

Redis实现分布式Session有2种方法,方法一是使用SpringSession实现,添加依赖和配置后,其余代码暂时不动,重新登录测试,会发现session已经存储在Redis上;方法二是将用户信息存入Redis,这里选用方法二。

添加redis配置类:

public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();//key序列化redisTemplate.setKeySerializer(new StringRedisSerializer());//value序列化redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());//hash类型 key序列化redisTemplate.setHashKeySerializer(new StringRedisSerializer());//hash类型 value序列化redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());//注入连接工厂redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}

优化登录功能:

因为大量接口需要用到User对象的传入,所以用到MVC对User进行解析。

/*** 自定义用户参数*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {@Autowiredprivate IUserService userService;@Overridepublic boolean supportsParameter(MethodParameter parameter) {Class<?> clazz = parameter.getParameterType();return clazz == User.class;}@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);String ticket = CookieUtil.getCookieValue(request, "userTicket");if (StringUtils.isEmpty(ticket)) {return null;}return userService.getByUserTicket(ticket, request, response);}
}/*** MVC配置类*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate UserArgumentResolver userArgumentResolver;@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(userArgumentResolver);}
}

用户完整登录接口实现代码:

public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {String mobile = loginVo.getMobile();String password = loginVo.getPassword();User user = userMapper.selectById(mobile);if (null == user) {throw new GlobalException(RespBeanEnum.LOGIN_ERROR);}//判断密码是否正确if (!MD5Util.formPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {throw new GlobalException(RespBeanEnum.LOGIN_ERROR);}//生成cookieString ticket = UUIDUtil.uuid();//将用户信息存入redis中redisTemplate.opsForValue().set("user:" + ticket, user);CookieUtil.setCookie(request, response, "userTicket", ticket);return RespBean.success(ticket);}/*** 功能描述: 根据cookie获取用户*/@Overridepublic User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {if (StringUtils.isEmpty(userTicket)) {return null;}User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);if (user != null) {CookieUtil.setCookie(request, response, "userTicket", userTicket);}return user;}

秒杀功能

商品列表页
逆向工程生成所需要的类:

GoodsVo:商品表和秒杀商品表的返回对象
public class GoodsVo extends Goods {private BigDecimal seckillPrice;private Integer stockCount;private Date startDate;private Date endDate;
}

GoodsMapper

public interface GoodsMapper extends BaseMapper<Goods> {/*** 功能描述: 获取商品列表*/List<GoodsVo> findGoodsVo();/*** 功能描述: 获取商品详情*/GoodsVo findGoodsVoByGoodsId(Long goodsId);
}
XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxxx.seckill.mapper.GoodsMapper"><!-- 通用查询映射结果 --><resultMap id="BaseResultMap" type="com.xxxx.seckill.pojo.Goods"><id column="id" property="id"/><result column="goods_name" property="goodsName"/><result column="goods_title" property="goodsTitle"/><result column="goods_img" property="goodsImg"/><result column="goods_detail" property="goodsDetail"/><result column="goods_price" property="goodsPrice"/><result column="goods_stock" property="goodsStock"/></resultMap><!-- 通用查询结果列 --><sql id="Base_Column_List">id, goods_name, goods_title, goods_img, goods_detail, goods_price, goods_stock</sql><!-- 获取商品列表 --><select id="findGoodsVo" resultType="com.xxxx.seckill.vo.GoodsVo">SELECTg.id,g.goods_name,g.goods_title,g.goods_img,g.goods_detail,g.goods_price,g.goods_stock,sg.seckill_price,sg.stock_count,sg.start_date,sg.end_dateFROMt_goods gLEFT JOIN t_seckill_goods AS sg ON g.id = sg.goods_id</select><!-- 获取商品详情 --><select id="findGoodsVoByGoodsId" resultType="com.xxxx.seckill.vo.GoodsVo">SELECTg.id,g.goods_name,g.goods_title,g.goods_img,g.goods_detail,g.goods_price,g.goods_stock,sg.seckill_price,sg.stock_count,sg.start_date,sg.end_dateFROMt_goods gLEFT JOIN t_seckill_goods AS sg ON g.id = sg.goods_idWHEREg.id =#{goodsId}</select>
</mapper>

GoodsService以及GoodsServiceImpl

public interface IGoodsService extends IService<Goods> {/*** 功能描述: 获取商品列表*/List<GoodsVo> findGoodsVo();/*** 功能描述: 获取商品详情*/GoodsVo findGoodsVoByGoodsId(Long goodsId);
}
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {@Autowiredprivate GoodsMapper goodsMapper;/*** 功能描述: 获取商品列表*/@Overridepublic List<GoodsVo> findGoodsVo() {return goodsMapper.findGoodsVo();}/*** 功能描述: 获取商品详情*/@Overridepublic GoodsVo findGoodsVoByGoodsId(Long goodsId) {return goodsMapper.findGoodsVoByGoodsId(goodsId);}
}

GoodsController(后面要加入redis缓存,代码见后)
图片无法访问的情况需要修改此配置类,在WebConfig中加入如下代码:

@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");}

商品列表页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="@{/{photo}(photo=${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>

商品详情页
主要是对秒杀情况的判定:秒杀前,秒杀时,秒杀后。后面因加入redis缓存,故放入部分代码:

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;}

秒杀功能实现
秒杀实现接口:先从数据表中通过秒杀商品的id获取当前秒杀的商品信息,将其库存减一(限制每人只能买一件),并根据SQL语句更新秒杀商品信息,并生成订单。

@Override
@Transactional
public Order seckill(User user, GoodsVo goods) {//秒杀商品表减库存SeckillGoods seckillGoods = seckillGoodsService.getOne(newQueryWrapper<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:进行秒杀时,会先判断User是否登录或者当前库存是否不足或者User是否重复抢购,再进行秒杀。

@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(newQueryWrapper<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";
}

优化阶段
缓存优化:避免频繁对数据库的访问,降低系统性能,对频繁读取,变更少的数据放入缓存中,变更多的数据不易放入缓存中,处理缓存一致性问题会比较麻烦。

页面缓存:

@RequestMapping(value = "/toList", produces = "text/html;charset=utf-8")@ResponseBodypublic String toList(Model model, User user,HttpServletRequest request, HttpServletResponse response) {//Redis中获取页面,如果不为空,直接返回页面ValueOperations valueOperations = redisTemplate.opsForValue();String html = (String) valueOperations.get("goodsList");if (!StringUtils.isEmpty(html)) {return html;}model.addAttribute("user", user);model.addAttribute("goodsList", goodsService.findGoodsVo());// return "goodsList";//如果为空,手动渲染,存入Redis并返回WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(),model.asMap());html = thymeleafViewResolver.getTemplateEngine().process("goodsList", context);if (!StringUtils.isEmpty(html)) {valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);}return html;}
/*** 功能描述: 跳转商品详情页*/@RequestMapping(value = "/toDetail2/{goodsId}", produces = "text/html;charset=utf-8")@ResponseBodypublic String toDetail2(Model model, User user, @PathVariable Long goodsId,HttpServletRequest request, HttpServletResponse response) {ValueOperations valueOperations = redisTemplate.opsForValue();//Redis中获取页面,如果不为空,直接返回页面String html = (String) valueOperations.get("goodsDetail:" + goodsId);if (!StringUtils.isEmpty(html)) {return html;}model.addAttribute("user", user);GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(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("remainSeconds", remainSeconds);model.addAttribute("secKillStatus", secKillStatus);model.addAttribute("goods", goodsVo);WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(),model.asMap());html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", context);if (!StringUtils.isEmpty(html)) {valueOperations.set("goodsDetail:" + goodsId, html, 60, TimeUnit.SECONDS);}return html;}

秒杀页面静态化,订单详情页面静态化,新建goodsDetail.htm,orderDetail.htm放入static目录下,并修改相应的controller。

库存超卖问题解决:

慕课网、乐字节Java电商秒杀项目相关推荐

  1. 慕课网,乐字节 Java电商秒杀项目

    慕课网.乐字节Java电商秒杀项目 技术点介绍: 前端:Thymeleaf,Bootstrap,Jquerry 后端:SpringBoot,MybatisPlus,Lombok 中间件:RabbitM ...

  2. JAVA电商秒杀实战(三)

    JAVA电商秒杀实战(三)- - 实现第三方登录 QQ登录 短信验证码登录 QQ登录 首先导入所需要的相关依赖: <dependency><groupId>org.apache ...

  3. Java电商秒杀系统性能优化(一)——电商秒杀系统框架回顾

    电商秒杀系统框架回顾 项目简介 外部依赖 框架回顾 项目要点 项目中存在的问题 小结 课程是免费的,课程地址如下:SpringBoot搭建电商秒杀项目,课程真的很棒,作者的思路很清晰,建议各位读者可以 ...

  4. java电商秒杀深度优化_【B0796】Java性能优化亿级流量秒杀方案及电商项目秒杀实操2020视频教程...

    Java视频教程名称:Java性能优化亿级流量秒杀方案及电商项目秒杀实操2020视频教程    java自学网[javazx.com]  性能视频教程   it教程 Java自学网收集整理 java论 ...

  5. Java电商秒杀系统项目笔记(不断完善中)

    主要业务包括用户的注册.登录:商品的创建.商品详细信息的展示.单个商品的详情与下单.采用前后端分离的设计思想,使用Ajax进行交互 后端:SpringBoot + Mybatis + Mysql + ...

  6. Java电商系统秒杀怎么做?

    有的时候,获取秒杀项目经历的代价是惨痛的- 比如我 前阵子我司(某十八线厂子)的运营策划了一场1元秒杀活动.一开始觉得肯定不会有很多人参与啊,于是没有准确的预估好用户的访问量.结果,你猜发生了什么:活 ...

  7. Java实现电商秒杀系统-jseckill

    1.前言 什么是秒杀?双十一,双十二天猫京东优惠大促销,大量的用户去抢夺少量的商品,在段时间内抢完,称之为秒杀.典型的高并发应用场景. 2.简介 电商秒杀系统,要求并发量特别大,用Java实现秒杀系统 ...

  8. java教程——电商秒杀系统介绍

    电商秒杀系统实战 QQ 1285575001 Wechat M010527 技术交流 QQ群599020441 纪年科技aming 高并发 缓存 异步化 前后端分离:MVVM框架 原生框架:jQ Bo ...

  9. 电商秒杀系统相关实现

    前言 本文主要就电商秒杀系统所涉及的相关技术进行探究,相关Demo地址如下: 个人实现版本:https://github.com/MrSorrow/seckill Github Star较高的版本,第 ...

最新文章

  1. Can‘t connect to HTTPS URL because the SSL module is not available
  2. hdu 2473(并查集+删除操作)
  3. error: expected ‘{‘ before ‘;‘ token
  4. 阿里云服务器(ECS)CentOS修改yum源为阿里源
  5. 微信小程序中this指向作用域问题this.setData is not a function报错
  6. iOS 之 获取View所在控制器
  7. 虚拟环境的常用命令 virtualenv命令
  8. 为什么工作7年又来开博客?
  9. 零基础学Arcgis(十二)地图标注与注记
  10. BM3D图像去噪算法原理及代码详解
  11. u盘测试工具linux,MultiBootUSB简单测试工具 V7.5.0 升级版
  12. Html查看dcm,DICOM图像浏览器(.dcm图像查看器)
  13. mysql 转大写_mysql将字符串转换为大写的方法
  14. bilibili视频流量数据潜望镜
  15. 关于如何设置网页自动切换背景图片
  16. 获取拼音-pinyin.js
  17. Python学习笔记:Python基础使用
  18. 应届生多次面试失败后的内心独白
  19. 无盘服务器跨vlan,实现不同的VLAN访问同一个服务器
  20. 去哪儿网业务大规模容器化最佳实践

热门文章

  1. 【概率论】正态分布的叠加性和正态分布的标准化
  2. mmcv包在linux下无法使用inshow展示报错:qt.qpa.xcb: could not connect to display
  3. 【转】MEMS陀螺仪的简要介绍(性能参数和使用)
  4. ConvFormer: Closing the Gap Between CNN and Vision Transformers
  5. kali 登录密码遗忘/kali登录密码重置
  6. Chrome浏览器安装插件时报错“无法从该网站添加应用、扩展程序和用户脚本”解决办法
  7. [20180920]航空航天与国防行业的股票排名
  8. React 可视化开发工具 Shadow Widget 非正经入门(之四:flux、mvc、mvvm)
  9. pitch, roll, yaw的区分
  10. 关于模糊理论及简单应用