首先创建项目Seckill
在l中配置
端口数据库信息mybatis配置
建立主启动类
@SpringBootApplication
@MapperScan("com.hspedu.seckill")
创建数据库
id,nickname,password,slatpassword进行MD5加密防止被盗
得先加入MD5依赖客户端----MD5(passworde明文+salt1)--->后端(md5(md5(password明文+salt1)+salt2)
编写POJO类(类似之前的Intity类)
@Data
@TableName("seckill_user")
User{@TableId(value = "id",type=IdType.ASSIGN_ID) //自增IDprivatie Long id;nickname;TelePhone;}
编写UserMapper接口
public interface UserMapper extends BaseMapper<User>{}
编写l文件实现其方法
<mapper namespace="com.hspedu.seckill.apper.UserMapper"><resultMap id="BaseResultMap" type=".../User"> //对应User的属性<id column="id" property="id" /> //column是数据库里名字,property是User的映射nickname;
创建枚举类方便返回不同结果
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum{//通用SUCCESS(200,"SUCCESS"),ERROR(500,"登录失败");//登录LOGIN_ERROR(500210,"用户id或密码错误"),MOBILE_ERROR(500211,"手机格式不对"),private final Integer code;private final String message;}
建立RespBean(返回信息的时候可能还带数据)
@Data
@NoArgsConstructor
@AllArgsConstructor
RespBean{private long code;private String message;private Object obj;//成功后同时携带数据public static RespBean success(Object data){return new RespBean(Code(),Message(),data);}//成功后不携带数据public static RespBean success(Object data){return new RespBean(Code(),Message(),null);}//失败-返回失败信息,不携带数据public static RespBean error(RespBeanEnum respBeanEnum){return new Code())}//失败-返回失败信息,携带数据public static RespBean error(RespBeanEnum respBeanEnum,Object data){return new Code(),data);}}
LoginVo (接收用户登录时发送的信息)
@Data public class LoginVo{private String mobile;private String passworde;}
ValidatorUtil(验证手机号正确性)
public class ValidatorUtil{private static finl Pattern mobile_pattern = Patternpile(...正则表达式)public static boolean isMobile(String mobile){if(!StringUtils.hasText(mobile)){return false;}Matcher matcher = mobile_pattern.matcher(mobile);return matcher.mathches();;}}
开始写Service层
public interface UserService extends IService<User>{//Iservice声明了很多方法,也可以加入自己定义的RespBean doLogin(LoginVo loginVo,requet,response)}
实现Service接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User>
implements UserService {@Resourceprivate UserMapper userMapper;@Override//重写接口的自定义方法doLogin(...){//先接收mobile和密码String mobile = Moboile();String pwd = Password();//判断手机号和密码是否为空if(!StringUtils.hasText(mobile) || !StringUtils.hasText(password)){(RespBeanEnum.LOGIN_ERROR);}//验证手机号格式if(!ValidatorUtil.isMobile(mobile)){(RespBeanEnum.LOGIN_ERROR);}//查询DBUser user = userMapper.selectById(mobile);if(null == user){(RespBeanEnum.LOGIN_ERROR);}//若用户存在则对比密码if(!){(RespBeanEnum.LOGIN_ERROR);}return RespBean.success(); //登录成功}}
然后是控制层
得先再pom引入spring-boot-starter-validation来验证@Controller
@RequetMapping("/login")
LoginController{@Resource //装备Userseviceprivate UserService userservice;@RequestMapping("/toLogin")public String toLogin(){ //到登录页面return "login";}@RequestMapping("/doLogin") //如果是返回信息则直接用RespBean返回@ResponseBody //意思为返回数据而非跳转页面public RespBean doLogin(@Valid LoginVo loginVo,request,response){ return userService.doLogin(loginVo,request,response); //验证}}
前端(就不写了)通过ajax请求将数据打到后端控制台的doLogin
得通过Maven的Complie编译到target目录
function doLgin(){$.ajax({url:"/login/doLogin"
type:"POST"
data:{mobaile:... //对应LoginVo的两个属性进行封装password:...
},
success:function(data){ //data是从后端拿到的信息,得校验de==200){ ssage);}
....
}})}
定义全局异常
@Data
@No
public GlobalException extend RuntimeException{private RespBeanEnum respBeanEnum; //返回的异常就是枚举类里的}
全局异常处理器
@RestControllerAdvice //加了这个注解,这个类就是全局异常
Public class GlobalExceptionHandler{@ExceptionHander(Exception.class)public RespBean ExceptionHandeler(Excepton e){GlobalException ex = (GlobalException) e;...处理逻辑(RespBeanEnum.ERROR);}
}
然后UerServiceImpl就可以这么修改
//(..)throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
用户验证成功后,保存Session记录用户信息,进入到商品列表
UUIDUtil标识用户的唯一性
public class UUIDUtil{public static String uuid(){return UUID.randomUUID().toString().repalce("-",""); //替换掉 -}
}
CookieUtil工具类可以更方便操作cookie
publica class CookieUtil{//很多代码都是固定的,就不写了}
通过Service保存UUID
UserServiceimpl{... {String ticket = UUIDUtil.uuid(); //每个用户生成唯一Session().setAttribute(ticket,user); //将登录成功的用户信息保存到Session,唯一标识,session的key就是ticket//通过Cookie工具类设置cookieCookieUtil.setCookie(requst,response,"userTicket",ticket);cookie名字 cookie值}}
新写个Controller
@Controller
@Req..("/goods")
public class GoodController{@RequstMapping("/toList")public String toList(HttpSession session,Model model, //要拿到Session@CookieValue("userTicket") String ticket){ //获取Cookie指定值if(!StringUtils.hasText(ticket)){ return "login";}User user = (Attribute(ticket); //看看有没有登录成功信息if(null == user){ //没有登录成功return "login"; //返回登录}model.addAttribute("user",user); //成功则将user放入modelreturn "goodlist";}}
先提出问题
集群TomcatA
client Nigix TomcatB
假如甲来秒杀,TomcatA没有记录他,好,它可以秒杀并记录,但TomcatB并未记录甲再请求可能导致超卖
服务器把某个用户的请求,交给Tomcat集群中的一个节点,以后此节点负责保存该用户session,可以利用负载均衡的源地址Hash算法实现,同一个ip地址请求发送到同一台服务器
集群中的服务器同步他们之间的session,使每台都保存所有用户Session
字面意思
集群TomcatA
client Nigix TomcatB Redis存储Session
故现在选择将用户Session信息统一保存到Redis进行管理,而不是分布式地存放到不同服务器
这里就需要安装redis-desktop-manager(Redis可视化操作工具)
redis:host:port:database:..
直接将用户登陆信息放到Redis利于操作
key = user:...
value
就得用到RedisTemplate,最好自定义配置,系统自带的不太好
@Configuration
public class RedisConfig{....}
在UserServiceImpl配置
UserServiceImpl{@Resourceprivate RedisTemplate redistemplate; //这是自己配置的修改这句Session().setAttribute(ticket,user) redisTemplate.opsForValue().set("user:"+ticket,user); //登录信息存到Redis}
这样Controller也要到Redis获取信息,现在UserService定义方法并在UerServiceImp实现
interface UserService{User getUserByCookie(String userTicket,requst,response)}UserServiceImpl{@Resource RedisTemplate redisTemplate;@OverrideUser getUer(...){ //获取redis值User user = (User)redisTemplate.opsForValue().get("user"+userTicket);//如果用户不为空,就重新设置cookie,刷新,根据业务需求来if(user != null){ CookieUtil.setCookie(requst,response,"userTicket",userTicket); } }}
GoodController{@Resourceprivate UserService userservice; //装配userservicetoList(Model model,@RequstHttpServletRequest request,HttpServletResponse response){//从redis获取用户User userByCookie = UerByCookie(ticket,requst,response);}}
商品属性
t_goods
id,
goods_name
goods_title
goods_imag
...
秒杀商品属性
t_seckill_goods
id
goods_id
seckill_price
start_data //秒杀开始时间
end_data //结束时间
...
在Java里pojo包实现他们两个的实体类
@Data
@TableName("t_goods")
Goods{@TableId(value = "id",type = IdType.}
@Date
SecKillGoods{@TableId(value = "id",type=IdType.AUTO)private Long
}
因为到时候在页面展示时秒杀价和原价是同时展现的,所以得合并两张表价格信息
vo类下新建GoodsVo(对应显示在秒杀商品列表信息)
@Data
@All
@No
public class GoodsVo extends Goods{ //先继承Goods再补全seckillGoods的startDate;endDate;...
}
Mapper层-Goods的
public interface GoodsMapper extends BaseMapper<Goods>{//获取商品列表-秒杀List<GoodsVo> findGoodsVo()}
配置其对应l文件
Mapper层-seckillGoods的
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods>{}
对应XMl文件
//通用查询映射结果
<resultMap id="BaseResultMap" type="com..pojo.SeckillGoods"><id column="id" property="id"/><result column="goods_name" property = "goodsName"/>...</resultMap>
Service层-Goods
public interface GoodsService extends IService<Goods>{//秒杀商品集合List<GoodVo> findGoodsVo();}
实现类
@Service public class GoodsServiceImpl extends ServiceImpl<GoodsMapper,Goods>implements Goodservice{@ResourcegoodsMapper;.. //装配Mapper@OvrridefindGoods{goodsmapper.findGoodsVo();} }
Service层SeckillGoods
public interface SeckillGoodsService extends IService<SeckillGoods>{}
@Service public class SeckillGoodsServiceImpl extends ServiceImpl<SeckillGoodsMapper,SeckillGoods>implements SeckillGoodservice{}
修改GoodsCtroller
GoodsSCtroller{@Resourceprivate GoodeService goodsService;@RequstMapping("/.."){//将商品列表信息,放入model,携带到下一模板使用model.addAttribute("goodsList",goosService.findGoodsVo());}
}
通过model携带的数据到goodsList.html将数据展示在页面
<tr th:each"goods,goodstStat" : ${goodsList}> //这里对应上面<td th:text=${dsName}
同时让前端接收到200后直接进入商品页面
success:function(data){de==200){Window.location.href="/goods/toList" }
}
在GoodsMapper加入方法获取商品详情
GoodsMapper{//获取指定商品详情GoodsVo findGoodsVoByGoodsId(Long goodsId);}
XML对应
修改Service层(Goods)及其实现层
GoodsService{GoodsVo findGoodsVoByGoodsId(Long goodsId)}
GoodsServiceImpl{GoodsVo findGoodsVoByGoodsId(Long goodsId){goodsMapper.findGoodsVoByGoodsId(goodsId);}}
然后就是Controller层
GoodsController{//进入商品详情页//因为前端有这个 href="'/goods/toDetail/'+${goods.id}"@RequstMapping("/toDetail/{goodsId}")public String toDetail(Model model,User user,@PathVariabel Long goodsId){ //User是自定义参数解析器包装request和response处理后来的if(user == null){ //判断有没有登陆return "login";}model.addAttribute("user",user); //model携带数据到前端GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);model.addAttribute("goods",goodsVo); //这里的名称要和前端匹配配合return "goodsDetail";} }
即可以在页面展示秒杀开始时间和秒杀倒计时
在GoodsController中做修改
..
{//返回商品详情时,同时返回商品秒杀状态和剩余时间//定义 secKillStatus秒杀状态 0:秒杀开始 1:秒杀进行中 2:秒杀结束//remainSeconds 剩余秒杀时间 -1:秒杀已结束Date startDate = StartDate(); //得到开始时间Date endDate = EndDate(); //得到结束时间Date nowDate = new Date();int secKillStatus = 0;int remainSeconds = 0;if(nowDate.before(startDate)){ //还没有开始秒杀remainSecond =(int)(Time()-Time())/1000; //还有多久开始秒杀}else if(nowDate.after(endDate)){ //秒杀已结束secKillStatus = 2;remainSeconds = -1;}else{secKillStatus = 1;remainSeconds = 0; }model.addAttribute("secKillStatus",secKillStatus); //通过model传给前端model.addAttribute("remainSeconds",remainSeconds);}
秒杀成功进入页面填写相关信息;秒杀失败返回信息(库存不够,重复购买等)
同样得创建两张表
t_order普通订单 和 t_seckill_order
因为用户可能是正常购买,也可能是秒杀
t_order普通订单{iduser_idgoods_}t_seckill_order秒杀表{id;user_UNIQUE KEY `seckill_uid_gid`(`user_id`,`good_id`)USNIG BTREE COMMENT '用户id'//商品id的唯一索引,解决同一个用户多次抢购}
创建他们对应的Entity实体类
Order 和 SeckillOrder
然后经典Mapper接口
public interface OrderMapper extends BaseMapper<Order>{}
public interface SeckillMapper extends BaseMapper<SeckillOrder>{}
Service层
public interface OrderService extends IService<Order>{//完成秒杀方法Order seckill(User user,GoodsVo goodsVo); //谁来买什么}
public interface SeckillOrderService extends IService<SeckillOrder>{}
Service实现类
@Service public class OrderServiceImplextends ServiceImpl<OrderMapper,Order>implements OrderService{@Resource private SeckillGoodsService seckillGoodsService; //装配他方便查库存 @ResourceOderMapper.. seckill(user,goodsVo){//查询秒杀商品库存量 ,判断是否够会在controller里判断SeckillGoods seckillGoods = Id());//完成基本秒杀操作seckillGoods.StockCount()-1); //库存减一seckillGoodsService.updateById(seckillGoods); //生成普通订单Order order = new Order();order.Id());order.Id());...orderMapper.insert(order); //保存订单//生成秒杀商品订单SeckillOrder seckillOrder = new SeckillOrder();seckillOrder.Id());...seckillOrderService.save(seckillOrder); //保存订单return order }}
Controller修改
@Controller @RequestMapping("/seckill") //这里根据前端跳转来 SeckillController{@Resource goodsService;seckillOrderService;orderService;//处理用户抢购/秒杀请求@RequstMapping("/doSeckill")public String doSeckill(Model model,User user,Long goodsId){if(user == null){ //用户没登录 return "login";} model.addAttribute("user",user); //user放入modelGoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); StockCount() < 1){ //判断库存model.addAttribute("errms",RespBeanEnum.Message());return "seckillFail"; //返回错误页面}One("user_id",user_id),"gdid.." )!=null){ // 判断是否为复购 / userid,goodsid是否存在model.addAttribute("errms",RespBeanEnum.Message());return "seckillFail";}//都通过就抢购Order order = orderService.seckill(user,goodsVo);if(order == null){ //抢购失败的话model.addAttribute("errms",RespBeanEnum.Message());return "seckillFail"; }model.addAttribute("order",order); //带入下一个页面model.addAttribute("goods",goods); ...return "orderDetail"; //跳转到订单详情页html} }
Apache基于Java开发的压力测试工具,用于对软件做压力测试的,可以测试静态和动态资源,可对服务器,网络或对象模拟巨大的负载
双击jmeter.bat即可启动
新增线程组线程数: 10Ramp-Up(秒): 0循环次数: 5HTTP请求默认值协议:http 名称:localhost 端口 8080HTTP请求HTTP请求:GET 路径:/goods/toList监听器-聚合报告监听器-察看结果树监听器-图形结果 //这四个可要可不要监听器-结果报告
还得加个cookie管理器不然登不进去
HTTP Cookie管理器名称 值 域 ...
数据库添加
1330000000 jack
1330000001 smith
创建配置文件text文件
1330000000,23e238db32414kd //第二个是cookie(票)值
1330000001,36aef4klmnnoikm
进行Jmeter配置
CSV 数据文件设置
文件名: ..text //就是上面那个
变量名称:userId,userTicket //userTicket值就是从text中取
分隔符: ,HTTP Cookie管理器
名称 值 域 路径
userTicket ${userTicket} //对应上面的userTicket值
得要2000个用户 先创建UserUtil,创建用户并登录得到userticket写入
public class UserUtil{... //用到直接复制粘贴即可
}
然后新增线程组
HTTP-秒杀HTTP请求 路径 GET /seckill/doSeckill参数名称 值 goodsId 1 //秒杀goodsId为1的商品
测试后发现会超卖
这就是高并发引起的问题,得解决
seckillGoods.StockCount()-1);//比如高并发可能会使20个线程同时拿到StockCount,然后20个才减去一个1,就是不具备原子性
多用户在查看商品列表和商品详细时,每一个用户都需要到DB查询,DB压力很大,但商品信息又不变化,可以通过Redis缓存页面来进行优化。直接将查询结果缓存到Redis进行返回。就是减少对数据库的访问。
原始:浏览器 后端程序 数据库改进:浏览器 后端程序 Redis 数据库//第一次查询通过Redis到数据库查然后返回缓存到Redis//后面的相同请求查询就可以到Redis得到
优化
Controller
GoodsController{....@Resourceprivate ThymeleafViewResolver thyleafViewResolver; //手动渲染需要的模板解析器@RequestMapping("/toList",produces="text/html;charset=utf-8") @ResponseBodypublic String toList(Model model,User user,request,response){//先到Redis看有没有缓存页面ValueOperaions vs = redisTemplate.opsForValue();String html = (("goodsList");if(StringUtils.hasText(html)){return html;}...//如果没有从Redis获取到,则手动渲染加入RedisWebContext webContext = //获取Web上下文
new WenContext(request,ServletContext,model.asMap());取出model数据 html = TemplaateEngine().process("goodsList",webContext);//拿到模板引擎 渲染模板 vs.set("goodsList",html,60,TimeUnit.SECONDS); //缓存到redis的key名称 60s更新一次return html;}
}
toDetail类似弄到Redis缓存去
还有个小问题:因为Redis60s更新一次,如果在这期间修改数据了,但用户期间拿不到最新数据怎么办?同样这个问题缓存在Redis的对象也有?
在Redis冷却期间修改了数据可以直接将Redis的数据删除这样Redis会重新从数据库,提前修改数据库数据即可
先修改Service
UserService{//方法,更新密码RespBean updatePWD(String userTicket,String pwd,request,response) //拿到userTicket直接从Redis找,requset可能会返回数据,密码为新密码}
//实现该方法 UserServiceImpl{updatePWD(...){User user = getUserByCookie(userTicket,rst,rse); //通过票据得到对应userif(user == null){ //不存在则抛异常throw new GlobalException(RespBeanEnum,MOBILE_NOT_EXIST);}//设置新密码 user.setPassword(MD5Util.inputPassToDBPass(Slat())); //更新到数据库userMapper.updateByid(user);//删除在Redis的该用户数据redisTemplate.delete("user:"+userTicket);}}
Controller层
UserController{@ResourceuserService@RequstMapping("/updpwd")@ResponseBodypublic RespBean updatePWD (String userTicket,String PWD,rst,rse){return userService.updatePWD(...//上面这四个)}
}
浏览器 过滤 seckill方法//这里可能 //这里可能20个请求才将库存减1请求冲过来
seckillGoods.StockCount()-1);//主要就是这句话会出问题 //比如高并发可能会使20个线程同时拿到StockCount,然后20个才减去一个1,就是不具备原子性
修改OrderServiceImpl
OrderServiceimp{//Mysql默认隔离级别 可重复读 执行update语句会在事务中锁定要更新的行,这样可防止其他会话在同一执行update,delete;boolean update = seckillGoodsService.update(new UpdateWa<SeckillGoods>().setSql("stock_count = stock_count-1").eq("goods_id",Id()).gt("stock_count",0));//只有更新成功才返回trueif(!update){return null; //如果不为真,防止订单再增加(下面的)}Order order = new Order()......//这里可以将秒杀订单放入Redis,这样查询某个用户是否秒杀过时,可直接到Redis查询redisTemplate.opsForValue().set("order:"Id()+":"Id(),seckillOrder);// 秒杀订单key => order:用户id:商品id}
继而修改Controller
SeckillController{("/doSeckill"){//判断用户是否复购,直接就从Redis中获取对应秒杀订单,若有,则不能继续秒杀SeckillOrder o = redisTemplate.opsForValue().get("order:"Id()+":"Id());if(null != o){ //说明用户已经秒杀过了model.addAttribute("errmsg"...); return "secKillFail"; //返回错误页面 }}
}
本文发布于:2024-02-01 11:39:46,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170675878736337.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |