高并发项目(其一)

Seckill

基本环境配置

首先创建项目Seckill

pom.xml导入相关的maven依赖

在application.yml中配置

端口

数据库信息

mybatis配置

建立主启动类

@SpringBootApplication
@MapperScan("com.hspedu.seckill")

创建数据库

id,nickname,password,slat

password进行MD5加密防止被盗
得先加入MD5依赖

客户端----MD5(passworde明文+salt1)--->后端(md5(md5(password明文+salt1)+salt2)

编写POJO类(类似之前的Intity类)

@Data
@TableName("seckill_user")
User{
 
 @TableId(value = "id",type=IdType.ASSIGN_ID)   //自增ID
         privatie Long id;

    nickname;
    TelePhone;

}

编写UserMapper接口

public interface UserMapper extends BaseMapper<User>{



}

编写UserMapper.xml文件实现其方法

<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(RespEnum.SUCCESS.getCode(),
                        RespEnum.SUCCESS.getMessage(),data);
   }


  //成功后不携带数据
  public static RespBean success(Object data){
            return new RespBean(RespEnum.SUCCESS.getCode(),
                        RespEnum.SUCCESS.getMessage(),null);
   }

  //失败-返回失败信息,不携带数据
    public static RespBean error(RespBeanEnum respBeanEnum){
            return new RespBean(respBeanEnum.getCode())
    }

//失败-返回失败信息,携带数据
    public static RespBean error(RespBeanEnum respBeanEnum,Object data){
            return new RespBean(respBeanEnum.getCode(),data);
    }
  
}

LoginVo (接收用户登录时发送的信息)

@Data
public class LoginVo{

      private String mobile;
      private String passworde;

}

ValidatorUtil(验证手机号正确性)

public class ValidatorUtil{

    private static finl Pattern mobile_pattern = Pattern.compile(...正则表达式)

    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 {
          
        @Resource
        private UserMapper userMapper;

        @Override
        //重写接口的自定义方法
        doLogin(...){
            
        //先接收mobile和密码
        String mobile = loginVo.getMoboile();
        String pwd    = loginVo.getPassword();
        
        //判断手机号和密码是否为空
        if(!StringUtils.hasText(mobile) || !StringUtils.hasText(password)){
                return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
 
          //验证手机号格式
        if(!ValidatorUtil.isMobile(mobile)){
          return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        //查询DB
        User user = userMapper.selectById(mobile);
        if(null == user){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        //若用户存在则对比密码
        if(!MD5Util.midPassToDBPASS...){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }


        return RespBean.success();        //登录成功
    }

}

然后是控制层

得先再pom引入spring-boot-starter-validation来验证


@Controller
@RequetMapping("/login")
LoginController{

            
    @Resource                                //装备Usersevice
    private 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是从后端拿到的信息,得校验
 if(data.code==200){                
    alert(data.message);

}
....
}


})

}

定义全局异常

@Data
@All...
@No
public GlobalException extend RuntimeException{

    private RespBeanEnum respBeanEnum;    //返回的异常就是枚举类里的

}

全局异常处理器

@RestControllerAdvice                     //加了这个注解,这个类就是全局异常
Public class GlobalExceptionHandler{

    @ExceptionHander(Exception.class)
    public RespBean ExceptionHandeler(Excepton e){
       GlobalException ex = (GlobalException) e;

        ...处理逻辑
    
    return RespBean.error(RespBeanEnum.ERROR);
    }
}

然后UerServiceImpl就可以这么修改

//return RespBean.error(..)

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();            //每个用户生成唯一ticket
        
       
         request.getSession().setAttribute(ticket,user); 
        //将登录成功的用户信息保存到Session,唯一标识,session的key就是ticket

        //通过Cookie工具类设置cookie
        CookieUtil.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 = (User)session.getAttribute(ticket);   //看看有没有登录成功信息
        if(null == user){    //没有登录成功
            return "login";    //返回登录
        }

        model.addAttribute("user",user);    //成功则将user放入model
        return "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进行管理,而不是分布式地存放到不同服务器

request.getSession().setAttribute(ticket,user)          //这个session保存到服务器的语句就得改

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

Spring整合Redis
pom.xml

spring-session-data-redis

application.yml配置redis

redis:
  host:
  port:
  database:
  ..

直接将用户登陆信息放到Redis利于操作

key = user:...

value=user...

就得用到RedisTemplate,最好自定义配置,系统自带的不太好

@Configuration
public class RedisConfig{
    
        ....

}

在UserServiceImpl配置

UserServiceImpl{

    @Resource
    private RedisTemplate redistemplate;        //这是自己配置的

    修改这句request.getSession().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;
   
    @Override
     User getUer(...){                    //获取redis值
     User user = (User)redisTemplate.opsForValue().get("user"+userTicket);

    //如果用户不为空,就重新设置cookie,刷新,根据业务需求来
    if(user != null){ 
    CookieUtil.setCookie(requst,response,"userTicket",userTicket);    
        }    
    }
  
}

GoodController{

    @Resource
    private UserService userservice;        //装配userservice
    
    toList(Model model,
            @Requst
            HttpServletRequest request,
            HttpServletResponse response){
        
            //从redis获取用户
        User userByCookie = userService.getUerByCookie(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.AUTO)
    id
    goodsname
    goodsTitle
    ...

}
@Date
@T...
SecKillGoods{
    
    @TableId(value = "id",type=IdType.AUTO)
    private Long id;
    goodsId
    seckillPrice
    ...
}

因为到时候在页面展示时秒杀价和原价是同时展现的,所以得合并两张表价格信息

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()

}

配置其对应Mapper.xml文件

GoodsMapper.xml

//通用查询映射结果
<resultMap id="..." type="com..pojo.Goods">
    <id column="id" property="id"/>
    <result column="goods_name" property = "goodsName"/>
    ...

</resultMap>

<select id="findGoodsVo" resultType="..vo.GoodsVo">

SELECT g.id,g.goods_name,g.goos_img,sg.start_date,sg.end_date
FROM t_goods g 
LEFT JOIN t_seckill_goods sg
ON g.id = sg.goods.id

</select>

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{

        @Resource
        goodsMapper;..      //装配Mapper


        @Ovrride
        findGoods{
        goodsmapper.findGoodsVo();

        }    
}

Service层SeckillGoods

public interface SeckillGoodsService extends IService<SeckillGoods>{



}
@Service
public class SeckillGoodsServiceImpl 
        extends ServiceImpl<SeckillGoodsMapper,SeckillGoods>
        implements SeckillGoodservice{

    }

修改GoodsCtroller

GoodsSCtroller{

    @Resource
    private GoodeService goodsService;

   @RequstMapping("/.."){

    //将商品列表信息,放入model,携带到下一模板使用
    model.addAttribute("goodsList",goosService.findGoodsVo());

    }
}

通过model携带的数据到goodsList.html将数据展示在页面

<tr th:each"goods,goodstStat" : ${goodsList}>  //这里对应上面
    <td th:text=${goods.goodsName}

同时让前端接收到200后直接进入商品页面

success:function(data){
     if(data.code==200){
        Window.location.href="/goods/toList"   
        
    }
}

然后就进入商品展示页面了

在GoodsMapper加入方法获取商品详情

GoodsMapper{

    //获取指定商品详情
    GoodsVo findGoodsVoByGoodsId(Long goodsId);


}

XML对应

GoodsMapper.xml

<select id="findGoodsVoByGoodsId" resultType="..vo.GoodsVo">

SELECT g.id,g.goods_name,g.goos_img,sg.start_date,sg.end_date
FROM t_goods g 
LEFT JOIN t_seckill_goods sg
ON g.id = sg.goods.id
WHERE g.id = #{goodsId}                //通过方法findGoodsVoByGoodsId的形参决定


</select>

修改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 = goodsVo.getStartDate();  //得到开始时间
  Date endDate = goodsVo.getEndDate();        //得到结束时间
  Date nowDate = new Date();
  int secKillStatus = 0;
  int remainSeconds = 0;

  if(nowDate.before(startDate)){        //还没有开始秒杀
    
     remainSecond =(int)(startDate.getTime()-nowDate.getTime())/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普通订单{
  id
  user_id
  goods_id
  goods_name
  ...
  

}


t_seckill_order秒杀表{

    id;
    user_id
    order_id
    ...
    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>{

    

}
OrderMapper.xml
<mapper namespace="com.....GoodMapper">
    
//通用映射结果集
<resultMap id="BaseResultMap" type="...Order">     //假如以后返回实体类类型Order则其
  <id column="id" property="id" />                   property 字段对应表格这些字段
  <result column="user_id" property="userId" />
  <result column="goods_id" property="goodsId" />

  ...

</mapper>

SeckillOrder.xml类似

Service层

public interface OrderService extends IService<Order>{

        //完成秒杀方法
        Order seckill(User user,GoodsVo goodsVo);    //谁来买什么

}
public interface SeckillOrderService extends IService<SeckillOrder>{

       

}

Service实现类

@Service
public class OrderServiceImpl
      extends ServiceImpl<OrderMapper,Order>
      implements OrderService{

      @Resource    
      private SeckillGoodsService seckillGoodsService; //装配他方便查库存        

      @Resource
       OderMapper..

      @Resource
        seckillOrderService...       

    
       seckill(user,goodsVo){

         //查询秒杀商品库存量 ,判断是否够会在controller里判断
       SeckillGoods seckillGoods  = seckillGoodsService.getOne(goodsVo.getId());
        
        //完成基本秒杀操作
       seckillGoods.setStackCount(seckillGoods.getStockCount()-1); //库存减一
       seckillGoodsService.updateById(seckillGoods);                

        //生成普通订单
       Order order = new Order();
       order.setUserId(user.getId());
       order.setGoodsId(goodsVo.getId());
        ...
  
       orderMapper.insert(order);                //保存订单
       
        //生成秒杀商品订单
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setGoodeId(goodVo.getId());
        ...
        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放入model
          GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);            

          if(goodsVo.getStockCount() < 1){        //判断库存
                model.addAttribute("errms",RespBeanEnum.ENTRY_STOCK.getMessage());
                return "seckillFail";            //返回错误页面
           }
          if(seckillOrderService.getOne("user_id",user_id),"gdid.." )!=null){          //                                                                      判断是否为复购                                                                                       /                                                               userid,goodsid是否存在
                    
          model.addAttribute("errms",RespBeanEnum.ENTRY_STOCK.getMessage());
          return "seckillFail";
        }


        //都通过就抢购
      Order order = orderService.seckill(user,goodsVo);
      if(order == null){                                        //抢购失败的话
        model.addAttribute("errms",RespBeanEnum.ENTRY_STOCK.getMessage());
        return "seckillFail";  
        }

        model.addAttribute("order",order);            //带入下一个页面
        model.addAttribute("goods",goods);        
        ...



            return "orderDetail";     //跳转到订单详情页html
   }
}

JMeter

Apache基于Java开发的压力测试工具,用于对软件做压力测试的,可以测试静态和动态资源,可对服务器,网络或对象模拟巨大的负载

双击jmeter.bat即可启动

模拟
新增线程组


线程数:       10

Ramp-Up(秒): 0
 
循环次数:     5

    HTTP请求默认值
    
    协议:http    名称:localhost 端口 8080

    HTTP请求

    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写入config.txt

public class UserUtil{

        ...    //用到直接复制粘贴即可
}

然后新增线程组

HTTP-秒杀

HTTP请求        路径
GET            /seckill/doSeckill

参数

  名称            值
goodsId           1                //秒杀goodsId为1的商品

测试后发现会超卖

这就是高并发引起的问题,得解决

seckillGoods.setStockCount(seckillGoods.getStockCount()-1);

//比如高并发可能会使20个线程同时拿到StockCount,
  然后20个才减去一个1,就是不具备原子性

页面优化

多用户在查看商品列表和商品详细时,每一个用户都需要到DB查询,DB压力很大,但商品信息又不变化,可以通过Redis缓存页面来进行优化。直接将查询结果缓存到Redis进行返回。就是减少对数据库的访问。


原始:

浏览器            后端程序          数据库



改进:

浏览器        后端程序       Redis      数据库


//第一次查询通过Redis到数据库查然后返回缓存到Redis

//后面的相同请求查询就可以到Redis得到

优化

Controller

GoodsController{
    
    ....
    @Resource
    private ThymeleafViewResolver thyleafViewResolver;   //手动渲染需要的模板解析器

    @RequestMapping("/toList",produces="text/html;charset=utf-8") 
    @ResponseBody
    public String toList(Model model,User user,request,response){
        
       //先到Redis看有没有缓存页面
       ValueOperaions vs = redisTemplate.opsForValue();
       String html = (String)valueOperations.get("goodsList");
       if(StringUtils.hasText(html)){
            return html;
       }
        ...

        //如果没有从Redis获取到,则手动渲染加入Redis
        WebContext webContext =                             //获取Web上下文
new WenContext(request,response,requst.getServletContext,model.asMap());
                                                            取出model数据 

    html =  thyleafViewResolver.getTemplaateEngine().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);   //通过票据得到对应user


    if(user ==  null){  //不存在则抛异常
        throw new GlobalException(RespBeanEnum,MOBILE_NOT_EXIST);
    }
        //设置新密码    
    user.setPassword(MD5Util.inputPassToDBPass(password,user.getSlat()));   
        //更新到数据库
    userMapper.updateByid(user);
    
        //删除在Redis的该用户数据
    redisTemplate.delete("user:"+userTicket);

    
  }

}

Controller层

UserController{

 @Resource
 userService

 @RequstMapping("/updpwd")
 @ResponseBody
 public RespBean updatePWD (String userTicket,String PWD,rst,rse){

      return userService.updatePWD(...//上面这四个)

   }
}
这下解决多用户高并发秒杀商品出现的超卖和多订单问题


浏览器           过滤            seckill方法
            //这里可能           //这里可能20个请求才将库存减1
             请求冲过来
seckillGoods.setStockCount(seckillGoods.getStockCount()-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",goodsVo.getId()).gt("stock_count",0));
    //只有更新成功才返回true

    if(!update){
    
     return null;    //如果不为真,防止订单再增加(下面的)

    }

     Order order = new Order()...
        ...
     
     //这里可以将秒杀订单放入Redis,这样查询某个用户是否秒杀过时,可直接到Redis查询
     redisTemplate.opsForValue().set
        ("order:"+user.getId()+":"+goodsVo.getId(),seckillOrder);
    //  秒杀订单key => order:用户id:商品id

}

继而修改Controller

SeckillController{

     ("/doSeckill"){

        //判断用户是否复购,直接就从Redis中获取对应秒杀订单,若有,则不能继续秒杀
       SeckillOrder o =  redisTemplate.opsForValue().get
            ("order:"+user.getId()+":"+goodsVo.getId());
          
        if(null != o){            //说明用户已经秒杀过了
            
           model.addAttribute("errmsg"...);   
            return "secKillFail";        //返回错误页面     
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值