1. Day15 秒杀前端
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制:库存限制、时间限制。
需求:
- 秒杀频道首页列出秒杀商品
- 点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
- 秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
- 当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
2. 秒杀商品存入缓存
秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
2.1. 秒杀服务搭建
- 在changgou_service_api项目下 新建
changgou_service_seckill_api
服务 添加公共依赖
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
在changgou_service_seckill_api服务下 添加
com.changgou.seckill.feign
和com.changgou.seckill.pojo
两个包将资源文件夹下的两个pojo类放入com.changgou.seckill.pojo包中
在changgou_parent项目下 新建服务
changgou_service_seckill
添加依赖
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common_db</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_service_order_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_service_seckill_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_service_goods_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
<!--oauth依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
- 创建包
com.changgou.seckill
然后创建启动类SecKillApplication
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
public class SecKillApplication {
public static void main(String[] args) {
SpringApplication.run(SecKillApplication.class, args);
}
//idwork
@Bean
public IdWorker idWorker() {
return new IdWorker(1, 1);
}
//设置redisTemplate序列化
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
- 设置application
server:
port: 9016
spring:
jackson:
time-zone: GMT+8
application:
name: seckill
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.130.128:3306/changgou_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8
username: root
password: root
main:
allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
redis:
host: 192.168.130.128
rabbitmq:
host: 192.168.130.128
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
client:
config:
default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
thread:
# 熔断器超时时间,默认:1000/毫秒
timeoutInMilliseconds: 20000
将author的公钥复制一份到resource目录下
创建
com.changgou.config
包 然后新建ResourceServerConfig
配置类
@Configuration
@EnableResourceServer
//开启方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
*
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
- 更改网关路径过滤类 添加秒杀工程过滤信息
进到到changgou_gateway_web项目下的 com.changgou.web.gateway.filter.URLFilter 过滤类中
添加 路径为/api/seckill
之前我们添加过 所有无需添加
- 更改网关的application配置文件 添加秒杀服务的路由转发
#秒杀微服务
- id: changgou_seckill_route
uri: lb://seckill
predicates:
- Path=/api/seckill/**
filters:
- StripPrefix=1
2.2. 时间操作
根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。
- 将资源文件夹下的DateUtil工具类 放到changgou_common的util包下
public class DateUtil {
/***
* 从yyyy-MM-dd HH:mm格式转成yyyyMMddHH格式
* @param dateStr
* @return
*/
public static String formatStr(String dateStr){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
try {
Date date = simpleDateFormat.parse(dateStr);
simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
return simpleDateFormat.format(date);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
/***
* 获取指定日期的凌晨
* @return
*/
public static Date toDayStartHour(Date date){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date start = calendar.getTime();
return start;
}
/***
* 时间增加N分钟
* @param date
* @param minutes
* @return
*/
public static Date addDateMinutes(Date date,int minutes){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minutes);// 24小时制
date = calendar.getTime();
return date;
}
/***
* 时间递增N小时
* @param hour
* @return
*/
public static Date addDateHour(Date date,int hour){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR, hour);// 24小时制
date = calendar.getTime();
return date;
}
/***
* 获取时间菜单
* @return
*/
public static List<Date> getDateMenus(){
//定义一个List<Date>集合,存储所有时间段
List<Date> dates = new ArrayList<Date>();
//循环12次
Date date = toDayStartHour(new Date()); //凌晨
for (int i = 0; i <12 ; i++) {
//每次递增2小时,将每次递增的时间存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
}
//判断当前时间属于哪个时间范围
Date now = new Date();
for (Date cdate : dates) {
//开始时间<=当前时间<开始时间+2小时
if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){
now = cdate;
break;
}
}
//当前需要显示的时间菜单
List<Date> dateMenus = new ArrayList<Date>();
for (int i = 0; i <5 ; i++) {
dateMenus.add(addDateHour(now,i*2));
}
return dateMenus;
}
/***
* 时间转成yyyyMMddHH
* @param date
* @return
*/
public static String date2Str(Date date){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
return simpleDateFormat.format(date);
}
}
2.2.1. 当前业务整体流程分析
1.查询所有符合条件的秒杀商品
1) 获取时间段集合并循环遍历出每一个时间段
2) 获取每一个时间段名称,用于后续redis中key的设置
3) 状态必须为审核通过 status=1
4) 商品库存个数>0
5) 秒杀商品开始时间>=当前时间段
6) 秒杀商品结束<当前时间段+2小时
7) 排除之前已经加载到Redis缓存中的商品数据
8) 执行查询获取对应的结果集
2.将秒杀商品存入缓存
2.3. 代码实现
2.3.1. 更改启动类,添加开启定时任务注解
@EnableScheduling
2.3.2. 定义定时任务类
秒杀工程新建task
包,并新建任务类SeckillGoodsPushTask
业务逻辑:
1)获取秒杀时间段菜单信息
2)遍历每一个时间段,添加该时间段下秒杀商品
2.1)将当前时间段转换为String,作为redis中的key
2.2)查询商品信息(状态为1,库存大于0,秒杀商品开始时间大于当前时间段,秒杀商品结束时间小于当前时间段,当前商品的id不在redis中)
3)添加redis
@Component
public class SeckillGoodsPushTask {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
// redis key的前缀
public static final String SECKILL_GOODS_KEY = "seckill_goods";
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0/30 * * * * ?")
public void loadSecKillGoodsToRedis() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//获取当前时间到 每2小时的5个时间集合
List<Date> dateMenus = DateUtil.getDateMenus();
for (Date dateMenu : dateMenus) {
//获取每个时间段名称 用于redis中作为key
String redisExtName = DateUtil.date2Str(dateMenu);
Example example = new Example(SeckillGoods.class);
Example.Criteria criteria = example.createCriteria();
// 状态要为1 表示已审核
criteria.andEqualTo("status", "1");
// 商品库存个数 要大于0
criteria.andGreaterThan("stockCount", 0);
// 秒杀商品开始时间 >= 当前时间段
criteria.andGreaterThanOrEqualTo("startTime", sdf.format(dateMenu));
// 并且秒杀商品结束时间 < 当前时间段+2
criteria.andLessThan("endTime", sdf.format(DateUtil.addDateHour(dateMenu, 2)));
// 获取当前时间段在redis的所有商品的key值 key为redis前缀+时间段名
Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys();
if (keys != null && keys.size() > 0) {
// 排除之前已经加载到Redis缓存中的商品 即在redis已经存在
criteria.andNotIn("id", keys);
}
// 符合以上条件的结果集合
List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);
// 添加到redis缓存中
for (SeckillGoods seckillGoods : seckillGoodsList) {
redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName, seckillGoods.getGoodsId(), seckillGoods);
}
}
}
}
3. 秒杀页面
秒杀商品首页会显示处于秒杀中以及未开始秒杀的商品。
3.1. 需求分析
秒杀首页需要显示不同时间段的秒杀商品信息,然后当用户选择不同的时间段,查询该时间段下的秒杀商品,实现过程分为两大过程:
1) 加载时间菜单
2)加载时间菜单下秒杀商品信息
3.1.1. 加载时间菜单分析
每2个小时就会切换一次抢购活动,所以商品发布的时候,我们将时间定格在2小时内抢购,每次发布商品的时候,商品抢购开始时间和结束时间是这2小时的边界。
每2小时会有一批商品参与抢购,所以我们可以将24小时切分为12个菜单,每个菜单都是个2小时的时间段,当前选中的时间菜单需要根据当前时间判断,判断当前时间属于哪个秒杀时间段,然后将该时间段作为选中的第1个时间菜单。
3.1.2. 加载对应秒杀商品分析
进入首页时,到后台查询时间菜单信息,然后将第1个菜单的时间段作为key,在Redis中查询秒杀商品集合,并显示到页面,页面每次点击切换不同时间段菜单的时候,都将时间段传入到后台,后台根据时间段获取对应的秒杀商品集合。
3.2. 秒杀渲染服务 - 渲染秒杀首页
- 创建工程
changgou_web_seckill
,用于秒杀页面渲染 添加依赖
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_service_seckill_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
- 创建
com.changgou.seckill.web
包 并新建启动类SecKillWebApplication
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.changgou.seckill.feign"})
public class SecKillWebApplication {
public static void main(String[] args) {
SpringApplication.run(SecKillWebApplication.class, args);
}
/**
* Feign拦截器
*/
@Bean
public FeignInterceptor feignInterceptor() {
return new FeignInterceptor();
}
/**
* 设置 redisTemplate 的序列化设置
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
- application
server:
port: 9104
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
spring:
jackson:
time-zone: GMT+8
thymeleaf:
cache: false
application:
name: seckill-web
main:
allow-bean-definition-overriding: true
redis:
host: 192.168.130.128
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
thread:
timeoutInMilliseconds: 60000
#请求处理的超时时间
ribbon:
ReadTimeout: 4000
#请求连接的超时时间
ConnectTimeout: 3000
- 添加静态化资源 在resource创建
templates
文件夹 并将资源文件夹的静态资源复制进去
- 将当前微服务添加到网关中
#秒杀渲染微服务
- id: changgou_seckill_web_route
uri: lb://seckill-web
predicates:
- Path=/api/wseckillgoods/**
filters:
- StripPrefix=1
3.3. 时间菜单实现
时间菜单显示,先运算出每2小时一个抢购,就需要实现12个菜单,可以先计算出每个时间的临界值,然后根据当前时间判断需要显示12个时间段菜单中的哪个菜单,再在该时间菜单的基础之上往后挪4个菜单,一直显示5个时间菜单。
3.3.1. 时间菜单获取
在changgou_web_seckill项目下创建 controller层包
再创建 控制层类 SecKillGoodsController
@Controller
@RequestMapping("/wseckillgoods")
public class SecKillGoodsController {
//跳转到秒杀首页
@RequestMapping("/toIndex")
public String toIndex() {
return "seckill-index";
}
//获取秒杀时间集合信息
@RequestMapping("/timeMenus")
@ResponseBody
public List<String> dataMenus() {
//获取当前时间段修改集合
List<Date> dateMenus = DateUtil.getDateMenus();
List<String> result = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Date dateMenu : dateMenus) {
result.add(sdf.format(dateMenu));
}
return result;
}
}
3.3.2. 页面加载时间菜单
修改seckill-index.html 第113行的代码
<!--秒杀时间-->
<div class="sectime">
<div class="item-time active"
v-for="(item,index) in dateMenus">
<div class="time-clock">{{item}}</div>
<div class="time-state-on">
<span class="on-text" v-if="index==0">快抢中</span>
<span class="on-over" v-if="index==0">距离结束:01:02:03</span>
<span class="on-text" v-if="index>0">即将开始</span>
<span class="on-over" v-if="index>0">距离开始:03:02:01</span>
</div>
</div>
</div>
修改当前页面的vue代码 编写请求
var app = new Vue({
el: '#app',
data() {
return {
goodslist: [],
dateMenus: [],
ctime: 0, //当前时间菜单选中的下标,
alltimes: []
}
},
methods: {
loadMenus: () => {
axios.get("/api/wseckillgoods/timeMenus").then((response) => {
app.dateMenus = response.data
})
}
},
create: () => {
this.loadMenus()
}
})