高并发项目(其一)

阅读: 评论:0

高并发项目(其一)

高并发项目(其一)

Seckill

基本环境配置

首先创建项目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

用户验证成功后,保存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";}}

分布式Session

先提出问题

                                      集群TomcatA
client           Nigix                    TomcatB

假如甲来秒杀,TomcatA没有记录他,好,它可以秒杀并记录,但TomcatB并未记录甲再请求可能导致超卖

解决:

1.Session绑定(使用较少)

服务器把某个用户的请求,交给Tomcat集群中的一个节点,以后此节点负责保存该用户session,可以利用负载均衡的源地址Hash算法实现,同一个ip地址请求发送到同一台服务器

2.Session复制(小型架构使用较多)

集群中的服务器同步他们之间的session,使每台都保存所有用户Session

3.前端存储(数据大小受cookie限制,用的较少)

字面意思

4.后端集中存储(安全容易水平拓展但有点复杂)
                                      集群TomcatA
client           Nigix                    TomcatB            Redis存储Session

故现在选择将用户Session信息统一保存到Redis进行管理,而不是分布式地存放到不同服务器

这里就需要安装redis-desktop-manager(Redis可视化操作工具)

Spring整合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}
}

JMeter

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 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23