目录
- 一、登录方式调整
- 二、生成秒杀订单
- 1、绑定秒杀商品
- 2、查看秒杀商品
- 3、订单秒杀
- ①移除seata相关
- ②生成秒杀订单
- ③前端页面秒杀测试
一、登录方式调整
第1步:从zmall-common的pom.xml中移除spring-session-data-redis
依赖
注意:
1)本章节中不采用spring-session方式,改用redis直接存储用户登录信息,主要是为了方便之后的jmeter压测;
2)这里只注释调用spring-session的依赖,保留redis的依赖;
第2步:在zmall-common公共模块中定义RedisConfig配置类
package com.zking.zmall.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> restTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
//String类型Key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
//String类型Value序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//Hash类型Key序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//Hash类型Value序列化
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
这里一定要注意,最后在将RedisConnectionFactory设置到RedisTemplate中,不要在最前做该步操作,不然会导致String和Hash类型的序列化无效,将采用默认的JdkSerializationRedisSerializer进行序列化,从而导致保存的key前缀出现乱码问题。细心!!!
第3步:在zmall-common公共模块中配置redis相关服务
IRedisServcie
package com.zking.zmall.service;
import com.zking.zmall.model.User;
public interface IRedisService {
/**
* 将登陆用户对象保存到Redis中,并以token来命名
* @param token
* @param user
*/
void setUserToRedis(String token, User user);
/**
* 根据token令牌从Redis中获取User对象
* @param token
* @return
*/
User getUserByToken(String token);
}
RedisServcieImple
package com.zking.zmall.service.impl;
import com.zking.zmall.model.User;
import com.zking.zmall.service.IRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisServiceImpl implements IRedisService {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public void setUserToRedis(String token, User user) {
String key="user:"+token;
redisTemplate.boundValueOps(key).set(user,7200, TimeUnit.SECONDS);
}
@Override
public User getUserByToken(String token) {
return (User) redisTemplate.opsForValue().get("user:"+token);
}
}
用户登录成功后,将用户对象保存到Redis中,并设置超时时间7200秒。
第4步:在zmall-common公共模块中配置,配置自定义参数解析UserArgumentResolver、WebConfig
UserArgumentResolver
package com.zking.zmall.config;
import com.zking.zmall.exception.BusinessException;
import com.zking.zmall.model.User;
import com.zking.zmall.service.IRedisService;
import com.zking.zmall.util.CookieUtils;
import com.zking.zmall.util.JsonResponseStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义用户参数类
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private IRedisService redisService;
/**
* 只有supportsParameter方法执行返回true,才能执行下面的resolveArgument方法
* @param methodParameter
* @return
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class<?> type = methodParameter.getParameterType();
return type== User.class;
}
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest req= (HttpServletRequest) nativeWebRequest.getNativeRequest();
//从cookie获取token令牌
String token = CookieUtils.getCookieValue(req, "token");
//判断cookie中的token令牌是否为空
if(StringUtils.isEmpty(token))
throw new BusinessException(JsonResponseStatus.TOKEN_ERROR);
//根据token令牌获取redis中存储的user对象,方便jmeter测试
User user = redisService.getUserByToken(token);
if(null==user)
throw new BusinessException(JsonResponseStatus.TOKEN_ERROR);
return user;
}
}
WebConfig
package com.zking.zmall.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Component
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("/static/**")
// .addResourceLocations("classpath:/static/");
}
}
第5步:用户登录业务调整,将spring-session方式更改为redis方式存储登录用户信息。
package com.zking.zmall.service.impl;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zking.zmall.mapper.UserMapper;
import com.zking.zmall.model.User;
import com.zking.zmall.service.IUserService;
import com.zking.zmall.util.CookieUtils;
import com.zking.zmall.util.JsonResponseBody;
import com.zking.zmall.util.JsonResponseStatus;
import com.zking.zmall.vo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;
/**
* <p>
* 服务实现类
* </p>
*
* @author xnx
* @since 2023-02-06
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private RedisServiceImpl redisService;
@Override
public JsonResponseBody<?> userLogin(UserVo user,
HttpServletRequest req,
HttpServletResponse resp) {
//1.判断用户账号和密码是否为空
if(StringUtils.isEmpty(user.getLoginName())||
StringUtils.isEmpty(user.getPassword()))
return new JsonResponseBody<>(JsonResponseStatus.USERNAME_OR_PWD_EMPTY);
//2.根据用户名查询数据对应的用户信息
User us = this.getOne(new QueryWrapper<User>()
.eq("loginName", user.getLoginName()));
//3.判断us用户对象是否为空
if(null==us)
return new JsonResponseBody<>(JsonResponseStatus.USERNAME_ERROR);
try {
//MD5加密转换处理
String pwd= MD5Utils.md5Hex(user.getPassword().getBytes());
//4.判断输入密码与数据库表存储密码是否一致
if(!us.getPassword().equals(pwd)){
return new JsonResponseBody<>(JsonResponseStatus.PASSWORD_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
return new JsonResponseBody<>(JsonResponseStatus.ERROR);
}
// //5.通过UUID生成token令牌并保存到cookie中
// String token= UUID.randomUUID().toString().replace("-","");
// //将随机生成的Token令牌保存到Cookie中,并设置1800秒超时时间
// CookieUtils.setCookie(req,resp,"token",token,7200);
// //6.将token令牌与spring session进行绑定并存入redis中
// HttpSession session = req.getSession();
// session.setAttribute(token,us);
//5.通过UUID生成token令牌并保存到cookie中
String token= UUID.randomUUID().toString().replace("-","");
//将随机生成的Token令牌保存到Cookie中,并设置1800秒超时时间
CookieUtils.setCookie(req,resp,"token",token,7200);
//6.将token令牌与spring session进行绑定并存入redis中
//HttpSession session = req.getSession();
//session.setAttribute(token,us);
//将token令牌与user绑定后存储到redis中,方便jmeter测试
redisService.setUserToRedis(token,us);
return new JsonResponseBody<>(token);
}
}
这里采用Redis方式直接存储登录用户信息,只为后续使用Jmeter压测时提供便利。正常运行使用项目还是可以使用spring-session方式。
第6步:修改商品服务zmall-product模块中的index方法,将之前从HttpSession中获取登录用户信息改换成User对象参数方式
@RequestMapping("/index.html")
public String index(Model model, User user){
System.out.println(user);
}
在调用index方法之前,先由自定义的参数解析器进行参数解析并返回解析结果User,所以在这里可直接在方法参数中获取的User对象。
第7步:重启zmall-user和zmall-product模块,完成用户登录后,直接在浏览器地址栏输入:http://zmall.com/product-serv/index.html,查看zmall-product模块中的控制台是否已经获取到登录用户对象信息。
二、生成秒杀订单
1、绑定秒杀商品
添加sellDetail.html页面到zmall-product模块中;实现首页秒杀商品展示,必须保证秒杀商品状态为已激活、且秒杀商品的活动时间为有效时间范围之内。
index.html
<#if kills??>
<#list kills as g>
<div class="sell_${g_index?if_exists+1}">
<div class="sb_img"><a href="${ctx}/sellDetail.html?pid=${g.item_id}"><img src="${g.fileName}" width="242" height="356" /></a></div>
<div class="s_price">¥<span>${g.price}</span></div>
<div class="s_name">
<h2><a href="${ctx}/sellDetail.html?pid=${g.item_id}">${g.name}</a></h2>
倒计时:<span>1200</span> 时 <span>30</span> 分 <span>28</span> 秒
</div>
</div>
</#list>
</#if>
sellDetail.html
<table border="0" style="width:100%; margin-bottom:50px;" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="315">
<div class="lim_name">${(prod.name)!}</div>
<div class="lim_price">
<span class="ch_txt">¥${(prod.price)!}</span>
<a href="javascript:void(0);" class="ch_a" pid="${(prod.item_id)!}" price="${(prod.price)!}">抢购</a>
</div>
<div class="lim_c">
<table border="0" style="width:100%; color:#888888;" cellspacing="0" cellpadding="0">
<tr>
<td width="35%">市场价 </td>
<td width="65%">折扣</td>
</tr>
<tr style="font-family:'Microsoft YaHei';">
<td style="text-decoration:line-through;">¥${(prod.price)!}</td>
<td>8.0</td>
</tr>
</table>
</div>
<div class="lim_c">
<div class="des_choice">
<span class="fl">型号:</span>
<ul>
<li class="checked">30ml<div class="ch_img"></div></li>
<li>50ml<div class="ch_img"></div></li>
<li>100ml<div class="ch_img"></div></li>
</ul>
</div>
<div class="des_choice">
<span class="fl">颜色:</span>
<ul>
<li>红色<div class="ch_img"></div></li>
<li class="checked">白色<div class="ch_img"></div></li>
<li>黑色<div class="ch_img"></div></li>
</ul>
</div>
</div>
<div class="lim_c">
<span class="fl">数量:</span><input type="text" value="${(prod.total)!}" class="lim_ipt" />
</div>
<div class="lim_clock">
距离团购结束还有<br />
<span>1200 时 30 分 28 秒</span>
</div>
</td>
<td width="525" align="center" style="border-left:1px solid #eaeaea;"><img src="${(prod.fileName)!}" width="460" height="460" /></td>
</tr>
</table>
web层
@RequestMapping("/index.html")
public ModelAndView toIndex(User user){
System.out.println("user:"+ JSON.toJSONString(user));
ModelAndView mv=new ModelAndView();
//获取热卖商品列表
List<Product> hot = productService.list(new QueryWrapper<Product>()
.orderByDesc("hot")
.last("limit 4"));
//获取显示秒杀商品
List<Map<String, Object>> maps = productService.queryKillProdNews();
mv.addObject("kills",maps);
mv.addObject("hots",hot);
mv.setViewName("index");
return mv;
}
service层
public interface IProductService extends IService<Product> {
void updateStock(Integer pid,Integer num);
/**
* 首页显示秒杀商品查询
* @return
*/
List<Map<String,Object>> queryKillProdNews();
/**
* 根据商品ID查询秒杀商品信息
* @param pid 秒杀商品ID
* @return
*/
Map<String,Object> queryKillProdById(Integer pid);
}
@Override
public List<Map<String, Object>> queryKillProdNews() {
return productMapper.queryKillProdNews();
}
@Override
public Map<String, Object> queryKillProdById(Integer pid) {
return productMapper.queryKillProdById(pid);
}
mapper层
@Repository
public interface ProductMapper extends BaseMapper<Product> {
// @MapKey("queryKillProdNews")
List<Map<String,Object>> queryKillProdNews();
Map<String,Object> queryKillProdById(Integer pid);
}
productMapper.xml
<select id="queryKillProdNews" resultType="java.util.Map">
select
k.id,k.item_id,p.name,p.price,p.fileName
from
zmall_kill k,zmall_product p
where k.item_id=p.id and
k.is_active=1 and
(now() between start_time and end_time)
order by k.create_time desc
limit 4
</select>
<select id="queryKillProdById" resultType="java.util.Map">
select
k.id,k.item_id,k.total,p.name,p.price,p.fileName
from
zmall_kill k,zmall_product p
where k.item_id=p.id and k.is_active=1 and item_id=#{value}
</select>
2、查看秒杀商品
点击限时秒杀中的秒杀商品,根据秒杀商品ID查询秒杀商品详情信息并跳转到sellDetail.html页面展示秒杀商品信息。
@RequestMapping("/sellDetail.html")
public String sellDetail(@RequestParam Integer pid, Model model){
//根据商品ID查询秒杀商品信息
Map<String, Object> prod = productService.queryKillProdById(pid);
model.addAttribute("prod",prod);
return "sellDetails";
}
3、订单秒杀
①移除seata相关
第1步:先注释掉zmall-order和zmall-product模块中的seata依赖
第2步:分别删掉zmall-order和zmall-product模块中resources目录下的bootstrap.xml和register.conf文件
seata分布式事务,进行jmeter压测秒杀订单接口效率太低(1000个并发请求,吞吐量为4.5/s)
②生成秒杀订单
将SnowFlake雪花ID生成工具类导入到zmall-common模块中utils,然后再生成秒杀订单时使用雪花ID来充当秒杀订单编号;在zmall-order模块中完成秒杀订单生成工作。
IOrderService
public interface IOrderService extends IService<Order> {
Order createOrder(Integer pid,Integer num);
/**
* 生成秒杀订单
* @param user 登陆用户对象
* @param pid 秒杀商品ID
* @param price 秒杀商品价格
* @return
*/
JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price);
}
OrderServiceImpl
@Autowired
private KillServiceImpl killService;
@Autowired
private OrderDetailServiceImpl orderDetailService;
@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price) {
//1.根据秒杀商品编号获取秒杀商品库存是否为空
Kill kill = killService.getOne(new QueryWrapper<Kill>().eq("item_id",pid));
if(kill.getTotal()<1)
throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
//2.秒杀商品库存减一
killService.update(new UpdateWrapper<Kill>()
.eq("item_id",pid)
.setSql("total=total-1"));
//3.生成秒杀订单及订单项
SnowFlake snowFlake=new SnowFlake(2,3);
Long orderId=snowFlake.nextId();
int orderIdInt = new Long(orderId).intValue();
//创建订单
Order order=new Order();
order.setUserId(user.getId());
order.setLoginName(user.getLoginName());
order.setCost(price);
order.setSerialNumber(orderIdInt+"");
this.save(order);
//创建订单项
OrderDetail orderDetail=new OrderDetail();
orderDetail.setOrderId(orderIdInt);
orderDetail.setProductId(pid);
orderDetail.setQuantity(1);
orderDetail.setCost(price);
orderDetailService.save(orderDetail);
return new JsonResponseBody();
}
OrderController
@RequestMapping("/createKillOrder/{pid}/{price}")
@ResponseBody
public JsonResponseBody<?> createKillOrder(User user,
@PathVariable("pid") Integer pid,
@PathVariable("price") Float price){
return orderService.createKillOrder(user,pid,price);
}
③前端页面秒杀测试
在sellDetail.html页面中添加订单秒杀JS方法。
<script>
$(function(){
$('.ch_a').click(function(){
let pid=$(this).attr('alt');
console.log(pid);
$.post('http://zmall.com/order-serv/createKillOrder',{pid:pid},function(rs){
console.log(rs);
if(rs.code===200)
alert('秒杀成功');
else
alert(rs.msg);
},'json');
});
});
</script>
这里虽然已经能正常展示秒杀效果,但是还是存在很多问题,比如:重复抢购问题等等问题。
注意:
$.post('http://user.zmall.com/userLogin',{
loginName:loginName,
password:password
},function(rs){
console.log(rs);
if(rs.code===200){
location.href='http://product.zmall.com/index.html';
}else{
alert(rs.msg);
}
},'json');
post方式不能跨二级域名发送请求,location.href可以跨二级域名发送请求;
$(function(){
$('.ch_a').click(function(){
let pid=$(this).attr("pid");
let price=$(this).attr("price");
console.log("pid=%s,price=%s",pid,price);
$.post('http://zmall.com/order-serv/createKillOrder/'+pid+'/'+price,{},function(rs){
console.log(rs);
if(rs.code===200)
alert('秒杀成功');
else
alert(rs.msg);
},'json');
});
});
$.post(‘http://zmall.com/order-serv/createKillOrder/’+pid+‘/’+price,{},function(rs){});能够正常访问;
$.post(‘http://order.zmall.com/createKillOrder/’+pid+‘/’+price,{},function(rs){});则会出现跨域问题;