1.秒杀:
秒杀概念:所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。
通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。秒杀商品通常有两种限制:库存限制、时间限制。
需求:
1.2 表结构说明
秒杀商品信息表
CREATETABLE`tb_seckill_goods`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`goods_id`bigint(20)DEFAULTNULLCOMMENT'spu ID',
`item_id`bigint(20)DEFAULTNULLCOMMENT'sku ID',
`title`varchar(100)DEFAULTNULLCOMMENT'标题',
`small_pic`varchar(150)DEFAULTNULLCOMMENT'商品图片',
`price`decimal(10,2)DEFAULTNULLCOMMENT'原价格',
`cost_price`decimal(10,2)DEFAULTNULLCOMMENT'秒杀价格',
`seller_id`varchar(100)DEFAULTNULLCOMMENT'商家ID',
`create_time`datetimeDEFAULTNULLCOMMENT'添加日期',
`check_time`datetimeDEFAULTNULLCOMMENT'审核日期',
`status`char(1)DEFAULTNULLCOMMENT'审核状态,0未审核,1审核通过,2审核不通过',
`start_time`datetimeDEFAULTNULLCOMMENT'开始时间',
`end_time`datetimeDEFAULTNULLCOMMENT'结束时间',
`num`int(11)DEFAULTNULLCOMMENT'秒杀商品数',
`stock_count`int(11)DEFAULTNULLCOMMENT'剩余库存数',
`introduction`varchar(2000)DEFAULTNULLCOMMENT'描述',
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8
(1)秒杀频道首页列出秒杀商品(4)点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。(5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。(6)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
秒杀商品订单表
CREATETABLE`tb_seckill_order`(
`id`bigint(20)NOTNULLCOMMENT'主键',
`seckill_id`bigint(20)DEFAULTNULLCOMMENT'秒杀商品ID',
`money`decimal(10,2)DEFAULTNULLCOMMENT'支付金额',
`user_id`varchar(50)DEFAULTNULLCOMMENT'用户',
`seller_id`varchar(50)DEFAULTNULLCOMMENT'商家',
`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',
`pay_time`datetimeDEFAULTNULLCOMMENT'支付时间',
`status`char(1)DEFAULTNULLCOMMENT'状态,0未支付,1已支付',
`receiver_address`varchar(200)DEFAULTNULLCOMMENT'收货人地址',
`receiver_mobile`varchar(20)DEFAULTNULLCOMMENT'收货人电话',
`receiver`varchar(20)DEFAULTNULLCOMMENT'收货人',
`transaction_id`varchar(30)DEFAULTNULLCOMMENT'交易流水',
PRIMARYKEY(`id`)
)ENGINE=InnoDBDEFAULTCHARSET=utf8;
需要解决的问题
如何解决分布式事务实现,分布式事务锁实现,商品超卖等问题
秒杀服务端:
2.秒杀商品存入redis缓存:
在秒杀服务中将秒杀商品存入mysql中,设置定时任务将秒杀商品从mysql中查询出来存入缓存中,以redis以hash类型进行数据存储,用户可以在前端看到展示的秒杀商品
秒杀服务搭建
1.changgou_service_seckill模块创建
1.启动类创建
1.1
redisTemplate序列化
//设置redisTemplate序列化
publicRedisTemplate<Object,Object>redisTemplate(RedisConnectionFactoryredisConnectionFactory){
//创建redis模板
RedisTemplate<Object,Object>template=newRedisTemplate<>();
//关联redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
//创建 序列化类
GenericToStringSerializergenericToStringSerializer=newGenericToStringSerializer(Object.class);
//序列化类 对象映射设置
//设置value转化格式和Key的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(newStringRedisSerializer());
template.afterPropertiesSet();
returntemplate;
}
2.鉴权公钥文件
config 配置类
packagecom.changgou.seckill.config;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.core.io.ClassPathResource;
importorg.springframework.core.io.Resource;
importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
importorg.springframework.security.config.annotation.web.builders.HttpSecurity;
importorg.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
importorg.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
importorg.springframework.security.oauth2.provider.token.TokenStore;
importorg.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
importorg.springframework.security.oauth2.provider.token.store.JwtTokenStore;
importjava.io.BufferedReader;
importjava.io.IOException;
importjava.io.InputStreamReader;
importjava.util.stream.Collectors;
@Configuration
@EnableResourceServer
//开启方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{
//公钥
privatestaticfinalStringPUBLIC_KEY="public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
publicTokenStoretokenStore(JwtAccessTokenConverterjwtAccessTokenConverter) {
returnnewJwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
publicJwtAccessTokenConverterjwtAccessTokenConverter() {
JwtAccessTokenConverterconverter=newJwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
returnconverter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
privateStringgetPubKey() {
Resourceresource=newClassPathResource(PUBLIC_KEY);
try{
InputStreamReaderinputStreamReader=newInputStreamReader(resource.getInputStream());
BufferedReaderbr=newBufferedReader(inputStreamReader);
returnbr.lines().collect(Collectors.joining("\n"));
}catch(IOExceptionioe) {
returnnull;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
publicvoidconfigure(HttpSecurityhttp)throwsException{
//所有请求必须认证通过
http.authorizeRequests()
.anyRequest()
.authenticated();//其他地址需要认证授权
}
}
3.通过网关访问
//所有需要传递令牌的地址
publicstaticStringfilterPath="/api/worder/**,/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
packagecom.changgou.web.gateway.filter;
publicclassUrlFilter{
//所有需要传递令牌的地址
publicstaticStringfilterPath="/api/worder/**,/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
publicstaticbooleanhasAuthorize(Stringurl){
String[]split=filterPath.replace("**","").split(",");
for(Stringvalue:split) {
if(url.startsWith(value)){
returntrue;//代表当前的访问地址是需要传递令牌的
}
}
returnfalse;//代表当前的访问地址是不需要传递令牌的
}
}
3.1更改网关配置文件,添加请求路由转发
#秒杀微服务
- id: changgou_seckill_route
uri: lb://seckill
predicates:
- Path=/api/seckill/**
filters:
- StripPrefix=1
changgou_service下
模块创建流程:
创建changgou_service_seckill
pom引入
common——db依赖 eureka依赖 changgou_service_order_api依赖 changgou_service_seckill_api依赖
changgou_service_goods_api依赖
spring-rabbit依赖
changgou_service_order_api模块创建
数据库表实体创建 (pojo)秒杀商品 秒杀订单
feign包
4.秒杀时间段计算:
package com.changgou.util;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Calendar;import java.util.Date;import java.util.List;
public class DateUtil {
从格式转成格式获取指定日期的凌晨
/***
* 时间增加N分钟
* @param date
* @param minutes
* @return
*/
publicstaticDateaddDateMinutes(Datedate,intminutes){
Calendarcalendar=Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE,minutes);// 24小时制
date=calendar.getTime();
returndate;
}
/***
* 时间递增N小时
* @param hour
* @return
*/
publicstaticDateaddDateHour(Datedate,inthour){
Calendarcalendar=Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR,hour);// 24小时制
date=calendar.getTime();
returndate;
}
/***
* 获取时间菜单
* @return
*/
publicstaticList<Date>getDateMenus(){
//定义一个List<Date>集合,存储所有时间段
List<Date>dates=newArrayList<Date>();
//循环12次
Datedate=toDayStartHour(newDate());//凌晨
for(inti=0;i<12;i++) {
//每次递增2小时,将每次递增的时间存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
}
//判断当前时间属于哪个时间范围
Datenow=newDate();
for(Datecdate:dates) {
//开始时间<=当前时间<开始时间+2小时
if(cdate.getTime()<=now.getTime()&&now.getTime()<addDateHour(cdate,2).getTime()){
now=cdate;
break;
}
}
//当前需要显示的时间菜单
List<Date>dateMenus=newArrayList<Date>();
for(inti=0;i<5;i++) {
dateMenus.add(addDateHour(now,i*2));
}
returndateMenus;
}
/***
* 时间转成yyyyMMddHH
* @param date
* @return
*/
publicstaticStringdate2Str(Datedate){
SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyyMMddHH");
returnsimpleDateFormat.format(date);
}
}
每个秒杀时间段间隔两小时,一天存在12个秒杀时间段,每个秒杀时间段有商品,每个秒杀商品存在开始时间结束时间(只要每个秒杀商品时间大于开始时间段,并且小于秒杀商品结束时间段,那么该秒杀商品就是属于这个秒杀时间段的)所以每个秒杀时间段中有哪些商品呢?上面提供了秒杀Utills
工具类中测试方法获取12个时间段:
publicstaticvoidmain(String[]args) {
//集合存储数据结果
List<Date>dateList=newArrayList<>();
//获取本日凌晨时间点
DatestartHour=DateUtil.toDayStartHour(newDate());
//循环12次
for(inti=0;i<12;i++) {
dateList.add(addDateHour(startHour,i*2));
}
for(Datedate:dateList) {
//输出打印 日期格式化
SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Stringformat=simpleDateFormat.format(date);
System.out.println(format);
}
}
需求:静态原型中只展示5个时间段
/***
* 获取时间菜单
* @return
*/
publicstaticList<Date>getDateMenus(){
//定义一个List<Date>集合,存储所有时间段
List<Date>dates=newArrayList<Date>();
//循环12次
Datedate=toDayStartHour(newDate());//凌晨
for(inti=0;i<12;i++) {
//每次递增2小时,将每次递增的时间存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
}
//判断当前时间属于哪个时间范围
Datenow=newDate();
for(Datecdate:dates) {
//开始时间<=当前时间<开始时间+2小时
if(cdate.getTime()<=now.getTime()&&now.getTime()<addDateHour(cdate,2).getTime()){
now=cdate;
break;
}
}
//当前需要显示的时间菜单
List<Date>dateMenus=newArrayList<Date>();
for(inti=0;i<5;i++) {
dateMenus.add(addDateHour(now,i*2));
}
returndateMenus;
}
5.秒杀商品存入缓存实现:
1.定义定时任务,查询符合条件的秒杀商品
逻辑:
1.获取时间段集合并循环遍历出每一个时间段
2.获取每个时间段名称,用于后续redis中Key的设置
3.秒杀商品状态必须为审核通过status=1
4.商品库存个数>0
5.商品秒杀开始时间>=当前时间段
6.秒杀商品结束<当前时间段+2
7.排除之前已经加载到Redis缓存中的商品数据
8.执行查询获取对应的结果集
2.将秒杀商品存入缓存
3.定义定时任务类 SecKillGoodsPushTask
1.秒杀服务启动类添加定时任务注解
2.定时任务包task 方法 方法注解Scheduled(设置定时执行时间):
publice void loadSecKillGoodsToRedis(){
1.创建秒杀时间段展示集合
2.遍历进行格式转化(使用工具类)
3.获取每个时间段名称作为redis的Key
4.进行查询 秒杀商品查询mapper进行注入
秒杀Mapper.selectByExample(examle);创建example传入操作的秒杀商品表实体类,获取查询条件对象,设置status状态,addGreaterThan(属性名 大于的值)商品库存个数>0,addGreaterThanOrEqualTo(开始时间属性名称,开始时间值ps:注意格式化)设置商品秒杀开始时间>=当前时间段,addLessThan(结束时间属性名称,)秒杀商品结束<当前时间段+2,排除之前已经加载到Redis缓存中的商品数据 注入RedisTemplate 定义常量 调用redisTemplate.boundHashOps(常量+redisKey).keys();获取redis中的值,拿到值进行判断有没有这个值(keys!=null&&keys.size()>0),执行查询获取对应的结果集,遍历结果集redisTemplate.opsForHash().put(大Key,秒杀商品Id小key,秒杀商品对象)
注意:大Key是boundHashOps中常量+之前获取的redisKey,小Key为秒杀商品实体中的Id,value为秒杀商品对象,这三个值可以通过opsForHash传入进行秒杀商品存入缓存的添加
}
6.秒杀商品列表展示:
需求:当前已经完成了秒杀时间段菜单的显示,当用户在切换时间段时按照不同的时间段展示不同时间段下的秒杀商品
实现流程:秒杀渲染服务基于Feign会调用秒杀服务,在秒杀服务定义方法
秒杀服务定义service接口 SeckillGoodsService
接口方法:List<SeckillGoods> list(String time)
实现类:SecKillGoodsServiceImpl
返回值时间集合list
表现层:SecKillGoodsController
返回值:查询商品列表集合给前端:seckillGoodsList
返回值:Result<List<SecKillGoods>>
Oauth2必须对所有请求进行放行,在配置类ResourceSeviceConfig configura方法中对请求路径进行放行:
再定义Feign接口进行Feign暴露 :
7.秒杀商品列表秒杀渲染服务显示数据
实现流程:在秒杀渲染服务controller中注入SecKillGoodsFeign,进行远程调用
SecKillGoodsController中定义方法
/**
* 秒杀时间段下商品列表显示
*/
@Autowired
privateSecKillGoodsFeignsecKillGoodsFeign;
@RequestMapping("/list")
@ResponseBody
publicResult<List<SeckillGoods>>list(Stringtime){
Result<List<SeckillGoods>>secKillGoodsList=secKillGoodsFeign.list(time);
returnsecKillGoodsList;
}
前端代码发起异步,经过前端网关通过类路径调用/api/wseckillgoods/list?time 传入时间参数获取 后台渲染服务controller中的返回值,前端还需要通过用户点击秒杀时间获取时间参数传入后台渲染服务controller,这样就完成了秒杀时间段下商品列表的数据展示了
测试:
bug:没查到?因为时间格式不对 使用时间格式化工具:
DateUtil.formatStr(time)
时间参数正确 展示成功
立即抢购实现秒杀下单Js