1 Star 37 Fork 13

贺向东 / mall-second-kill-project

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MulanPSL-2.0

环境搭建

依赖安装

引入thymeleaf组件、web组件、mysql依赖、mybatis-plus依赖、lombok组件、md5组件、自定义注解组件、redis组件。

设置配置文件,设置服务端口、名称、数据库连接、配置xml文件路径、日志级别

统一返回结果对象

返回结果类型编码对象

package com.hehehe.secondkill.vo;


public enum RespBeanEnum {
    SUCCESS(200,"SUCCESS"),
    ERROR(500,"服务端异常"),
    LOGIN_ERROR(500210,"帐号或密码错误"),
    MOBILE_ERROR(500310,"手机格式错误"),
    BIND_ERROR(500410,"参数校验异常"),
    EMPTY_STOCK(500510, "库存不足"),
    HAS_SECKILL(500610, "已经秒杀过了"),
    PASSWORD_UPDATE(500710,"更新密码失败"),
    USER_TIME_OUT(500810,"用户登录过期"),
    PATH_ERROR(500910, "路径错误"),
    ERROR_CAPTCHA(500110,"验证码错误"),
    REQUEST_FAST(501000,"请求频繁");

    private final Integer code;
    private final String message;

    RespBeanEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
    @Override
    public String toString() {
        return "RespBeanEnum{" +
                "code=" + code +
                ", message='" + message + '\'' +
                '}';
    }
}

返回结果内容对象

package com.hehehe.secondkill.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {

    private long code;
    private String message;
    private Object obj;

    //成功返回
    public static RespBean success() {
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), null);
    }

    public static RespBean success(Object obj) {
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), obj);
    }

    //因为失败各有不同,所以传入枚举
    public static RespBean error(RespBeanEnum respBeanEnum) {
        return new RespBean(respBeanEnum.getCode(), respBeanEnum.getMessage(), null);
    }

    public static RespBean error(RespBeanEnum respBeanEnum, Object obj) {
        return new RespBean(respBeanEnum.getCode(), respBeanEnum.getMessage(), obj);
    }
}

逆向工程生成基本结构

登录功能

安全加密

因为http是明文传输的,也就是中间的路由可以看到我们用户提交的帐号密码信息,因此我们选择对用户的密码进行MD5加密。

使用两次MD5加密:

客户端:PASS=MD5(密码+固定Salt)在前端实现。防止用户密码直接在网络中传输。

服务端:PASS=MD5(用户输入+随机salt)在后端实现。防止数据库泄露用户密码。

MD5Util.java

package com.hehehe.secondkill.utils;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Component;

@Component
public class MD5Util {
    public static String md5(String src) {
        return DigestUtils.md5Hex(src);
    }

    //盐
    private static final String salt="hexiangdong";

    //对原理密码加密一次,后端接收的就是这个密码
    public static String inputPassToFromPass(String inputPass) {
        String str = "" + salt.charAt(0) + salt.charAt(1) + inputPass + salt.charAt(2) + salt.charAt(3);
        return md5(str);
    }

    //对加密过的密码再加密一次,然后可以放到数据库中了
    public static String formPassToDBPass(String formPass, String salt) {
        String str = salt.charAt(0) + salt.charAt(1) + formPass + salt.charAt(2) + salt.charAt(3);
        return md5(str);
    }

    //对原始密码直接加密两次,放入数据库中
    public static String inputPassDBPass(String inputPass, String salt) {
        String fromPass = inputPassToFromPass(inputPass);
        String dbPass = formPassToDBPass(fromPass, salt);
        return dbPass;
    }

    public static void main(String[] args) {
        //91b1e895ad031ed5dfac5f1273e40485
        System.out.println(inputPassToFromPass("123456"));
        System.out.println(formPassToDBPass("cf6473ca0856077cdd4fb2f9c4dfaeae",salt));
        System.out.println(inputPassDBPass("123456", salt));
    }
}

后端手机号码格式验证

前端代码是可见的,只有前端校验是不够的,用户完全可以修改前端代码跳过校验。因此后端也需要校验。

编写校验注解IsMobile

①新建Annotation文件,IsMobile

package com.hehehe.secondkill.validator;

import com.hehehe.secondkill.vo.IsMobileValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    boolean require() default true;

    String message() default "手机号码格式错误";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

IsMobileValidator就是校验时的判断的逻辑

②编写IsMobileValidator.java

package com.hehehe.secondkill.vo;

import com.hehehe.secondkill.utils.ValidatorUtil;
import com.hehehe.secondkill.validator.IsMobile;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean require = false;
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        require = constraintAnnotation.require();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(require) {
            return ValidatorUtil.isMobiile(value);
        } else {
            if(StringUtils.isEmpty(value)) {
                return true;
            } else {
                return ValidatorUtil.isMobiile(value);
            }
        }
    }
}

③ValidatorUtil里面使用正则表达式判断

package com.hehehe.secondkill.utils;

import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ValidatorUtil {
    private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
    public static boolean isMobiile(String mobile) {
        if(StringUtils.isEmpty(mobile)) {
            return false;
        }
        Matcher matcher = mobile_pattern.matcher(mobile);
        return matcher.matches();
    }
}

④在controller中加上@Valid注解,表示需要对传入的参数进行校验,当然LoginVo类里面的mobile属性也要加上@IsMobile

@RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin(@Valid LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        RespBean res = userService.doLogin(loginVo,request,response);
        return res;
    }

分布式session问题

使用cookie+session来解决,将用户信息存入redis

导入CookieUtil.java工具类、UUIDUtil.java工具类

service层实现

@Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();

        User user = baseMapper.selectById(mobile);
        if(user == null) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }
        if(!MD5Util.formPassToDBPass(formPass, user.getSalt()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }
        //生成Cookie
        String ticket = UUIDUtil.uuid();
        //将用户信息放入redis
        redisTemplate.opsForValue().set("user:"+ticket,user);
        //request.getSession().setAttribute(ticket, user);
        //其实就是找到请求的发起地址,给发请求的地址设置一个cookie,后面的请求都让他带上这个userTicket
        CookieUtil.setCookie(request, response, "userTicket", ticket);
        return RespBean.success(ticket);
    }
@Override
    public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {
        if(StringUtils.isEmpty(userTicket)) {
            return null;
        }
        User user = (User)redisTemplate.opsForValue().get("user:" + userTicket);
        if(user != null) {
            CookieUtil.setCookie(request, response,"userTicket",userTicket);
        }
        return user;
    }

统一异常处理

ControllerAdvice 和 @ExceptionHandler 注解。

使用 ErrorController类 来实现

区别:

  1. @ControllerAdvice 方式只能处理控制器抛出的异常。此时请求已经进入控制器中。
  2. ErrorController类 方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误
  3. 如果应用中两者共同存在,则 @ControllerAdvice 方式处理控制器抛出的异常, ErrorController类 方式处理未进入控制器的异常
  4. @ControllerAdvice 方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常 信息,自由度更大。

创建GlobalException异常类

package com.hehehe.secondkill.exception;

import com.hehehe.secondkill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
    private RespBeanEnum respBeanEnum;
}

创建GlobalExceptionHandler异常拦截器

package com.hehehe.secondkill.exception;

import com.hehehe.secondkill.vo.RespBean;
import com.hehehe.secondkill.vo.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public RespBean ExceptionHandler(Exception e) {
        if(e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
        } else if(e instanceof BindException) {
            BindException be = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
            respBean.setMessage("参数校验异常:" + be.getAllErrors().get(0).getDefaultMessage());
            return respBean;
        }
        return RespBean.error(RespBeanEnum.ERROR);
    }
}

商品相关功能

商品列表

GoodsController

@Controller
@RequestMapping("/goods")
public class GoodsController {
 /**
 * 跳转登录页
 *
 * @return
 */
    @RequestMapping("/toList")
    public String toLogin(HttpSession session, Model model, 
    	@CookieValue("userTicket") String ticket) {//去请求体里面的cookie值
        if (StringUtils.isEmpty(ticket)) {//没有cookie说明就没有登陆,回去登录去
        	return "login";
        }
        //从服务器的session中获取
        User user = (User) session.getAttribute(ticket);
        if (null == user) {
            return "login";
        }
        model.addAttribute("user", user);
        return "goodsList";
    }

后面我们有引入了redis作为分布式session管理,将用户信息从服务器session放到了redis中,因此修改获取用户信息的方法

@RequestMapping("/toList")
public String toLogin(HttpServletRequest request,HttpServletResponse
	response, Model model, @CookieValue("userTicket") String ticket) {
    if (StringUtils.isEmpty(ticket)) {
    	return "login";
    }
    //从redis中获取
    User user = userService.getByUserTicket(ticket,request,response);
    if (null == user) {
    	return "login";
    }
    model.addAttribute("user", user);
    return "goodsList";
}

优化登录功能

我们每次访问页面,都要先获取cookie中的userTicket,然后使用这个userTicket去redis获取用户,如果每个controller都这样写会非常的臃肿。因此我们可以将这部分内容放到拦截器中实现。

UserArgumentResolver

@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private IUserService userService;
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz == User.class;
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, 
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest, 
        WebDataBinderFactory binderFactory) throws Exception {
        
        HttpServletRequest request =
        webRequest.getNativeRequest(HttpServletRequest.class);
        
        HttpServletResponse response =
        webRequest.getNativeResponse(HttpServletResponse.class);
        
        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        
        if (StringUtils.isEmpty(ticket)) {
        	return null;
        }
        return userService.getByUserTicket(ticket, request, response);
    }
}

然后编写WebConfig文件,将这个拦截器注册到配置中

@Configuration
@EnableWebMvc
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("/**").addResourceLocations("classpath:/static/");
    }

}

优化商品列表功能

@RequestMapping("/toList")
public String toLogin(Model model, User user) {
    if (null == user) {
    	return "login";
    }
    model.addAttribute("user", user);
    return "goodsList";
}

秒杀功能

商品详情

@RequestMapping("/toDetail/{goodsId}")
public String toDetail(Model model, User user, @PathVariable Long goodsId) {
	model.addAttribute("user", user);
   	GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
   	model.addAttribute("goods", goods);
   	Date startDate = goods.getStartDate();
   	Date endDate = goods.getEndDate();
   	Date nowDate = new Date();
   	//秒杀状态
   	int secKillStatus = 0;
   	//剩余开始时间
   	int remainSeconds = 0;
   	//秒杀还未开始
   	if (nowDate.before(startDate)) {
    	remainSeconds = (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.addAttribute("remainSeconds",remainSeconds);
    return "goodsDetail";
}

秒杀功能实现

controller层doSeckill方法

@RequestMapping("/doSeckill")
public String doSeckill(Model model, User user, Long goodsId) {
    if (user == null) {
    	return "login";
    }
    model.addAttribute("user", user);
    GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
    //判断库存
    if (goods.getStockCount() < 1) {
        model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
        return "seckillFail";
    }
    
    //判断是否重复抢购
    SeckillOrder seckillOrder = seckillOrderService.getOne(
        new QueryWrapper<SeckillOrder>().eq("user_id",user.getId()).
        eq("goods_id",goodsId));
    
    if (seckillOrder != null) {
        model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
        return "seckillFail";
    }
    Order order = orderService.seckill(user, goods);
    model.addAttribute("order",order);
    model.addAttribute("goods",goods);
    return "orderDetail";
}

service层seckill()方法

@Override
@Transactional
public Order seckill(User user, GoodsVo goods) {
    //秒杀商品表减库存
    SeckillGoods seckillGoods = seckillGoodsService.getOne(new
    QueryWrapper<SeckillGoods>().eq("goods_id",goods.getId()));
    seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
    seckillGoodsService.updateById(seckillGoods);
    //生成订单
    Order order = new Order();
    order.setUserId(user.getId());
    order.setGoodsId(goods.getId());
    order.setDeliveryAddrId(0L);
    order.setGoodsName(goods.getGoodsName());
    order.setGoodsCount(1);
    order.setGoodsPrice(seckillGoods.getSeckillPrice());
    order.setOrderChannel(1);
    order.setStatus(0);
    order.setCreateDate(new Date());
    orderMapper.insert(order);
    //生成秒杀订单
    SeckillOrder seckillOrder = new SeckillOrder();
    seckillOrder.setOrderId(order.getId());
    seckillOrder.setUserId(user.getId());
    seckillOrder.setGoodsId(goods.getId());
    seckillOrderService.save(seckillOrder);
    return order;
   }
}

大体上看没有问题,但是有没有思考并发的情况呢?一定会出现超卖的,兄弟!!

速度优化

1.页面优化

1.1商品详情页面缓存

因为商品详情页面几乎是静态的,所以可以将整个html的内容放入redis中

//商品详情
    @RequestMapping(value = "/toDetail/{goodsId}", produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toDetail(Model model, User user, @PathVariable Long goodsId, HttpServletRequest request, HttpServletResponse response) {
        if(user == null) {
            return "login";
        }
        //redis中获取页面
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String html = (String) valueOperations.get("goodsDetail:" + goodsId);
        if(!StringUtils.isEmpty(html)) {
            return html;
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        model.addAttribute("goods", goodsVo);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();

        int secKillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;
        if(nowDate.before(startDate)) {
            secKillStatus = 0;
            remainSeconds = (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.addAttribute("remainSeconds", remainSeconds);
        WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(),model.asMap());
        String newHtml = viewResolver.getTemplateEngine().process("goodsDetail", webContext);
        if(!StringUtils.isEmpty(newHtml)) {
            valueOperations.set("goodsDetail:"+goodsId, newHtml, 60, TimeUnit.SECONDS);
        }
        return newHtml;
    }

1.2商品详情页面静态化

将整个页面放入redis实际上放的是html源代码,如果这个页面过大,他的html源代码也会很长,在网络传输中的速度就会变慢。我们可以将这个页面直接变成静态的页面,具体的数据使用ajax请求来获取,只更新极少量的数据就可以了。

@RequestMapping("/detail/{goodsId}")
    @ResponseBody
    public RespBean toDetail(Model model, User user, @PathVariable Long goodsId) {
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();
        int secKillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;
        if(nowDate.before(startDate)) {
            secKillStatus = 0;
            remainSeconds = (int)((startDate.getTime() - nowDate.getTime()) / 1000);
        } else if(nowDate.after(endDate)) {
            secKillStatus = 2;
            remainSeconds = -1;
        } else {
            secKillStatus = 1;
            remainSeconds = 0;
        }
        DetailVo detailVo = new DetailVo();
        detailVo.setUser(user);
        detailVo.setGoodsVo(goodsVo);
        detailVo.setRemainSeconds(remainSeconds);
        detailVo.setSecKillStatus(secKillStatus);
        return RespBean.success(detailVo);
    }

这个是商品详情页面,秒杀页面也是可以静态化的

 @RequestMapping("/doSeckill2")
    public String doSeckill2(Model model, User user, Long goodsId) {
        if(user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        model.addAttribute("goods", goodsVo);
        //判断库存
        if(goodsVo.getStockCount() <= 0) {
            model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return "secKillFail";
        }
        //判断当前用户是否已经秒杀过
        QueryWrapper<SeckillOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", user.getId());
        wrapper.eq("goods_id", goodsId);
        SeckillOrder seckillOrder = seckillOrderService.getOne(wrapper);
        if(seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.HAS_SECKILL.getMessage());
            return "secKillFail";
        }
        //下订单
        Order order = orderService.seckill(user,goodsVo);
        model.addAttribute("order", order);
        return "orderDetail";
    }

订单详情静态化

//订单信息
    @RequestMapping("/detail")
    @ResponseBody
    public RespBean detail(User user, Long orderId) {
        if(user == null) {
            return RespBean.error(RespBeanEnum.USER_TIME_OUT);
        }

        OrderDetailVo detail = orderService.getDetail(orderId);

        return RespBean.success(detail);

    }

前端文件就不写了,详情可以看源代码。

这一点看完建议先看安全优化的1和2

2.秒杀优化

前文我们已经解决了超卖问题和单一用户多次购买问题。我们使用唯一性索引的方式来防止用户多次购买问题。但是如果一个用户发出的请求次数过多,那么后面的请求会直接访问数据库,虽然唯一性索引会保证安全,但是让数据库去处理这些大量的无效请求不太好。

因此当用户成功秒杀到商品的时候,我们将用户id和商品id作为key放到redis中,后续的秒杀请求需要先判断redis是否已经存在这个订单,存在就返回错误即可。

在秒杀service中加入以下代码

//生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goodsVo.getId());
seckillOrder.setUserId(user.getId());
seckillOrderService.save(seckillOrder);
//放入redis中
redisTemplate.opsForValue().set("order" + user.getId() + ":" + goodsVo.getId(), seckillOrder);

在秒杀的controller接口中加入以下代码

ValueOperations valueOperations = redisTemplate.opsForValue();
//判断redis中是存在该用户的订单
SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:"+user.getId()+":"+goodsId);
if(seckillOrder != null) {
    return RespBean.error(RespBeanEnum.HAS_SECKILL);
}

3.服务优化

3.1秒杀商品库存入redis

系统初始化完成后,将所有的秒杀的商品的库存信息放入redis中。

让SeckillController实现InitializingBean接口,重写里面的afterPropertiesSet方法

//加载完毕后执行,将商品库存数量加载到redis
@Override
public void afterPropertiesSet() throws Exception {
    List<GoodsVo> goodsVos = goodsService.getGoodsVo();
    if(goodsVos.size() == 0) {
    	return ;
    }
    ValueOperations valueOperations = redisTemplate.opsForValue();
    for(GoodsVo goodsVo : goodsVos) {
        valueOperations.set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
        emptyMap.put(goodsVo.getId(), false);
    }
}

3.2使用redis预减库存

使用redis预减库存,如果库存不足,直接返回错误。

但是这一点需要使用redis+lua脚本实现原子操作。

stock.lua

if (redis.call("exists",KEYS[1])==1) then
    local stock = tonumber(redis.call("get",KEYS[1]));
    if (stock > 0) then
        redis.call("incrby",KEYS[1],-1);
        return stock;
    end;
        return 0;
end;

在RedisConfig.java中添加这个lua脚本

@Bean
public DefaultRedisScript<Long> script() {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    //lock.lua脚本位置和application.properties同级目录
    redisScript.setLocation(new ClassPathResource("stock.lua"));
    redisScript.setResultType(Long.class);
    return redisScript;
}

在秒杀接口中判断

//使用lua脚本
Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
if(stock == 0) {
    emptyMap.put(goodsId, true);
    //valueOperations.increment("seckillGoods:" + goodsId);
    return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

3.3使用本地内存判断库存

如果在redis中预减库存是发现库存已经是0了,后续的请求还是会访问redis,虽然redis确实很快,但是访问redis是需要网络传输的,肯定没有本地内存块。

我们可以将没有库存的商品放入本地HashMap中,如果判断哈希表中存在这个商品,那就是没库存了,不用再看redis了

3.4使用rabbitmq处理秒杀请求

创建MQSender

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
        //发送秒杀信息
    public void sendSeckillMessage(String message) {
        log.info("发送消息:" + message);
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
    }

}

创建MQReceiver

@Service
@Slf4j
public class MQReceiver {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private IOrderService orderService;

    //进行下单
    @RabbitListener(queues = "seckillQueue")
    public void receive(String message) {
        log.info("接收到订单消息:",message);
        SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);
        Long goodsId = seckillMessage.getGoodsId();
        User user = seckillMessage.getUser();

        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        if(goodsVo.getStockCount() < 1) {
            return ;
        }
        //再次判断
        ValueOperations valueOperations = redisTemplate.opsForValue();
        SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:"+user.getId()+":"+goodsId);
        if(seckillOrder != null) {
            return ;
        }

        //下单操作
        try {
            orderService.seckill(user, goodsVo);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            return ;
        }

    }
}

最后前端轮询查询秒杀结果

//查询用户秒杀的结果
@RequestMapping("/result")
@ResponseBody
public RespBean getResult(User user, String goodsId) {
    if(user == null) {
        return RespBean.error(RespBeanEnum.USER_TIME_OUT);
    }
    String key = "order" + user.getId() + ":" + goodsId;
    ValueOperations valueOperations = redisTemplate.opsForValue();
    Long orderId = -1l;
    if(valueOperations.get(key) != null) {
        SeckillOrder seckillOrder = (SeckillOrder) valueOperations.get(key);
        orderId = seckillOrder.getOrderId();
    } else if((int)valueOperations.get("seckillGoods:" + goodsId) <= 0) {
        orderId = -1l;
    } else {
        orderId = 0l;
    }
    return RespBean.success(orderId);
}

看完看安全优化3

安全优化

1.超卖问题

原始的方案是先查询数据库中库存数量是否大于0,大于零的话再执行减库存的操作,这两个操作不是原子的,在高并发情况下一定会出现超卖问题的。

我们改进思路,使用sql语句直接更新数据库,将对应商品的库存减一,如果更新成功的话说明还没有超卖。但是使用sql语句更新会发生并发问题吗?答案是不会的,因为使用update关键字是,在mysql中是当前读,是要给这一行加行锁的。所以没有并发问题。

将service中减库存的操作改为如下内容

//判断库存是否大于0
UpdateWrapper<SeckillGoods> wrapper2 = new UpdateWrapper<>();
wrapper2.setSql("stock_count="+"stock_count-1");
wrapper2.eq("id", seckillGoods.getId());
wrapper2.gt("stock_count", 0);
boolean secKillres = seckillGoodsService.update(wrapper2);
if(!secKillres) {
    return null;
}

2.单用户多次秒杀问题

在之前的代码中,判断用户是不是已经秒杀过我们是这样做的

//判断当前用户是否已经秒杀过
QueryWrapper<SeckillOrder> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", user.getId());
wrapper.eq("goods_id", goodsId);
SeckillOrder seckillOrder = seckillOrderService.getOne(wrapper);
if(seckillOrder != null) {
    model.addAttribute("errmsg", RespBeanEnum.HAS_SECKILL.getMessage());
    return "secKillFail";
}

也就是查询数据库,看数据库中存不存在该用户相关的订单,如果存在就返回错误。毋庸置疑,一定是存在并发问题的。

我们可以在秒杀商品订单表中添加唯一性索引,将这个工作交给mysql处理,我们可以在秒杀商品订单表中以用户id列和商品id列组合建立一个唯一性索引就可以解决了。

看完之后看速度优化的2

3.接口隐藏

我们在前端的一些重要的接口地址手机直接暴露的,如果一些专业人员使用脚本不断地请求我们的接口,会导致刚开始秒杀就将商品秒杀完。

我们可以对接口隐藏,用户想要秒杀商品,首先需要获取这个秒杀的接口,采用UUID随机生成的策略。生成后的接口绑定上用户id和商品id,放入redis。

@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId) {
    if (user == null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
    }
    //使用UUID生成随机值并使用MD5加密一下然后放入redis
    String str = orderService.createPath(user,goodsId);
    return RespBean.success(str);
}

在秒杀接口加入以下内容,首先是映射地址,加上path,path就是我们上一步说的用户获取的属于他自己的接口地址。然后在redis中取出这个path,并判断是否和用户id和商品id相等

@RequestMapping("/{path}/doSeckill")
@ResponseBody
public RespBean doSeckill(@PathVariable String path, User user, Long goodsId) {
    if(user == null) {
    	return RespBean.error(RespBeanEnum.USER_TIME_OUT);
    }

    ValueOperations valueOperations = redisTemplate.opsForValue();
    //检查path是否正确
    Boolean check = orderService.checkPath(user, path, goodsId);
    if(!check) {
    	return RespBean.error(RespBeanEnum.PATH_ERROR);
    }
    //判断当前用户是否已经秒杀过
    SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:"+user.getId()+":"+goodsId);
    if(seckillOrder != null) {
    	return RespBean.error(RespBeanEnum.HAS_SECKILL);
    }

    if(emptyMap.get(goodsId)) {
    	return RespBean.error(RespBeanEnum.EMPTY_STOCK);
    }
            //redis预减库存
    //        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
            //使用lua脚本
    Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
    if(stock == 0) {
        emptyMap.put(goodsId, true);
        //valueOperations.increment("seckillGoods:" + goodsId);
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
    }
            //下订单
    SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
    mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));
    return RespBean.success(0);
}

4.接口限流

基本限流逻辑

ValueOperations valueOperations = redisTemplate.opsForValue();
//限制访问次数,5秒内访问5次
String uri = request.getRequestURI();
//方便测试
captcha = "0";
Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
if (count==null){
    valueOperations.set(uri + ":" + user.getId(),1,5,TimeUnit.SECONDS);
}else if (count<5){
    valueOperations.increment(uri + ":" + user.getId());
}else {
    return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
}

但是很多接口都需要用到这一部分逻辑,如果都写一遍,就太麻烦了,我们将他改为注解就很棒!

创建AccessLimit注解

//运行时的注解
@Retention(RetentionPolicy.RUNTIME)
//放在方法上的注解
@Target(ElementType.METHOD)
public @interface AccessLimit {
    int second();
    int maxCount();
    boolean needLogin() default true;
}

创建拦截器,捕获AccessLimit注解,并进行相关限流操作

@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断他是不是一个被拦截的一个方法
        if(handler instanceof HandlerMethod) {
            User user = getUser(request, response);
            UserContext.setUser(user);
            HandlerMethod hm = (HandlerMethod) handler;
            //获取这个方法上面的注解
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            //看有没有这个注解
            if(accessLimit == null) {
                return true;
            }
            //拿到这个注解的相关描述
            int second = accessLimit.second();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();
            if(needLogin) {
                if(user == null) {
                    render(response, RespBeanEnum.USER_TIME_OUT);
                    return false;
                }
            }

            //开始限流处理,核心代码
            ValueOperations valueOperations = redisTemplate.opsForValue();
            //发起请求的地址,限制访问次数,
            String uri = request.getRequestURI();
            Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
            String key = uri + ":" + user.getId();
            if(count == null) {
                valueOperations.set(key, 1, second, TimeUnit.SECONDS);
            } else if(count < maxCount) {
                valueOperations.increment(key);
            } else {
                render(response, RespBeanEnum.REQUEST_FAST);
                return false;
            }
        }

        return true;
    }

    //构建返回对象
    private void render(HttpServletResponse response, RespBeanEnum error) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        RespBean respBean = RespBean.error(error);
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(respBean));
        writer.flush();
        writer.close();
    }

    private User getUser(HttpServletRequest request, HttpServletResponse response) {
        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        if(StringUtils.isEmpty(ticket)) {
            return null;
        }
        return userService.getUserByCookie(ticket,request,response);
    }
}

创建UserContext,里面是一个ThreadLocal变量,用于不同的线程存储自己的User信息

public class UserContext {
    private static ThreadLocal<User> userHolder = new ThreadLocal<User>();

    public static void setUser(User user) {
        userHolder.set(user);
    }
    public static User getUser() {
        return userHolder.get();
    }
}

RabbitMQ使用

安装

①下载erlang

https://www.erlang-solutions.com/resources/download.html

安装erlang

yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm

检测erlang

erl

②安装RabbitMQ

下载地址http://www.rabbitmq.com/download.html

安装rabbitmq

install rabbitmq-server-3.8.5-1.el7.noarch.rpm

安装UI插件

rabbitmq-plugins enable rabbitmq_management

③启动rabbitmq服务

systemctl start rabbitmq-server.service

检测服务

systemctl status rabbitmq-server.service

访问15672端口,帐号guest 密码guest

在/etc/rabbitmq目录下创建一个rabbitmq.config文件

vim /etc/rabbitmq rabbitmq.config
加入以下内容
[{rabbit, [{loopback_users, []}]}].

重启reabbitmq服务

systemctl restart rabbitmq-server.service

依赖和配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
#rabbitmq配置
spring.rabbitmq.host=101.132.146.181
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672

编写RabbitMQ的java配置文件RabbitMQConfig.java

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //key的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value的序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //hash类型 key的序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //hash类型 value序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        //注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    @Bean
    public DefaultRedisScript<Long> script() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //lock.lua脚本位置和application.properties同级目录
        redisScript.setLocation(new ClassPathResource("stock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

简单测试

1.在config包下创建RabbitMQConfig.java文件

@Configuration
public class RabbitMQConfig {
    @Bean
    public Queue queue() {
        return new Queue("queue", true);
    }
}

2.在rabbitmq包下创建MQSender.java文件

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg) {
        log.info("发送消息:" + msg);
        //这个"queue"就是在配置文件中新建的Queue名字
        rabbitTemplate.convertAndSend("queue", msg);
    }
}

3.在rabbitmq包下创建MQReceiver.java文件

@Service
@Slf4j
public class MQReceiver {

    //接受队列"queue"中的消息
    @RabbitListener(queues="queue")
    public void receive(Object msg) {
        log.info("接收消息:" + msg);
    }
}

4.去Controller中测试

//测试发送消息
@RequestMapping("/mq")
@ResponseBody
public void mq() {
    mqSender.send("hello");
}

交换机测试

Fanout模式

1.在config包下创建RabbitMQConfig.java文件,创建两个队列和一个交换机,并将这两个队列绑定到交换机上,发送消息时会将消息发给所有绑定的队列中。

@Configuration
public class RabbitMQConfig {

    private static final String QUEUE01 = "queue_fanout01";
    private static final String QUEUE02 = "queue_fanout02";
    private static final String EXCHANGE = "fanoutExchange";

    @Bean
    public Queue queue01() {
        return new Queue(QUEUE01);
    }

    @Bean
    public Queue queue02() {
        return new Queue(QUEUE02);
    }

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE);
    }

    @Bean
    public Binding binding01() {
        return BindingBuilder.bind(queue01()).to(fanoutExchange());
    }

    @Bean
    public Binding binding02() {
        return BindingBuilder.bind(queue02()).to(fanoutExchange());
    }
}

2.在rabbitmq包下创建MQSender.java文件

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg) {
        log.info("发送消息:" + msg);
        //发送给交换机
        rabbitTemplate.convertAndSend("fanoutExchange", "", msg);
    }
}

3.在rabbitmq包下创建MQReceiver.java文件

@Service
@Slf4j
public class MQReceiver {
    
    @RabbitListener(queues = "queue_fanout01")
    public void receive01(Object msg) {
        log.info("QUEUE01接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_fanout02")
    public void receive02(Object msg) {
        log.info("QUEUE02接收消息:" + msg);
    }
    
}

4.去Controller中测试

@RequestMapping("/mq/fanout")
@ResponseBody
public void mq01() {
    mqSender.send("hello");
}

topic模式

1.在config包下创建RabbitMQTopicConfig.java文件,创建两个队列、两个路由匹配规则和一个交换机,并将这两个队列连同匹配规则绑定到交换机。

@Configuration
public class RabbitMQTopicConfig {

    private static final String QUEUE01 = "queue_topic01";
    private static final String QUEUE02 = "queue_topic02";
    private static final String EXCHANGE = "topicExchange";
    private static final String ROUTINGKEY01 = "#.queue.#";
    private static final String ROUTINGKEY02 = "*.queue.#";

    @Bean
    public Queue queue01() {
        return new Queue(QUEUE01);
    }

    @Bean
    public Queue queue02() {
        return new Queue(QUEUE02);
    }

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Binding binding01() {
        return BindingBuilder.bind(queue01()).to(topicExchange()).with(ROUTINGKEY01);
    }

    @Bean
    public Binding binding02() {
        return BindingBuilder.bind(queue02()).to(topicExchange()).with(ROUTINGKEY02);
    }
}

2.在rabbitmq包下创建MQSender.java文件

@Service
@Slf4j
public class MQSender {
    public void send03(Object msg) {
        log.info("发送(QUEUE01接收):",msg);
        rabbitTemplate.convertAndSend("topicExchange", "queue.red.message", msg);
    }

    public void send04(Object msg) {
        log.info("发送(被两个QUEUE接收):",msg);
        rabbitTemplate.convertAndSend("topicExchange", "message.queue.green", msg);
    }
}

3.在rabbitmq包下创建MQReceiver.java文件

@Service
@Slf4j
public class MQReceiver {
    @RabbitListener(queues = "queue_topic01")
    private void receive05(Object msg) {
        log.info("QUEUE01接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_topic02")
    private void receive06(Object msg) {
        log.info("QUEUE02接收消息:" + msg);
    }
}

4.去Controller中测试

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    MQSender mqSender;    
    
	//topic模式
    @RequestMapping("/mq/topic01")
    @ResponseBody
    public void mq04() {
        mqSender.send03("Hello");
    }

    @RequestMapping("/mq/topic02")
    @ResponseBody
    public void mq05() {
        mqSender.send04("Hello");
    }
}

header模式

1.在config包下创建RabbitMQHeaderConfig.java文件,创建两个队列和一个交换机,并将这两个队列连同匹配规则绑定到交换机。和topic中的匹配规则不同的是,header中的匹配规则是使用键值对的匹配规则。

@Configuration
public class RabbitMQHeadersConfig {

    private static final String QUEUE01 = "queue_hearder01";
    private static final String QUEUE02 = "queue_hearder02";
    private static final String EXCHANGE = "headerExchange";

    @Bean
    public Queue queue01() {
        return new Queue(QUEUE01);
    }

    @Bean
    public Queue queue02() {
        return new Queue(QUEUE02);
    }

    @Bean
    public HeadersExchange headersExchange() {
        return new HeadersExchange(EXCHANGE);
    }

    @Bean
    public Binding binding01() {
        Map<String, Object> map = new HashMap<String, Object>(){{
            put("color", "red");
            put("speed", "low");
        }};
        return BindingBuilder.bind(queue01()).to(headersExchange()).whereAny(map).match();
    }

    @Bean
    public Binding binding02() {
        Map<String, Object> map = new HashMap<String, Object>(){{
            put("color", "red");
            put("speed", "fast");
        }};
        return BindingBuilder.bind(queue02()).to(headersExchange()).whereAll(map).match();
    }
}

2.在rabbitmq包下创建MQSender.java文件

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg) {
        log.info("发送消息:" + msg);
        rabbitTemplate.convertAndSend("fanoutExchange", "", msg);
    }

    public void send01(Object msg) {
        log.info("发送red消息:" + msg);
        rabbitTemplate.convertAndSend("directExchange", "queue.red", msg);
    }

    public void send02(Object msg) {
        log.info("发送green消息:" + msg);
        rabbitTemplate.convertAndSend("directExchange", "queue.green", msg);
    }

    public void send03(Object msg) {
        log.info("发送(QUEUE01接收):",msg);
        rabbitTemplate.convertAndSend("topicExchange", "queue.red.message", msg);
    }

    public void send04(Object msg) {
        log.info("发送(被两个QUEUE接收):",msg);
        rabbitTemplate.convertAndSend("topicExchange", "message.queue.green", msg);
    }

    public void send05(Object msg) {
        log.info("发送(两个队列接收):", msg);
        MessageProperties properties = new MessageProperties();
        properties.setHeader("color", "red");
        properties.setHeader("speed", "fast");
        Message message = new Message(((String)msg).getBytes(), properties);
        rabbitTemplate.convertAndSend("headerExchange", "", message);
    }

    public void send06(Object msg) {
        log.info("发送(QUEUE01接收):", msg);
        MessageProperties properties = new MessageProperties();
        properties.setHeader("color", "red");
        properties.setHeader("speend", "normal");
        Message message = new Message(((String)msg).getBytes(), properties);
        rabbitTemplate.convertAndSend("headerExchange", "", message);
    }
}

3.在rabbitmq包下创建MQReceiver.java文件

@Service
@Slf4j
public class MQReceiver {

    @RabbitListener(queues="queue")
    public void receive(Object msg) {
        log.info("接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_fanout01")
    public void receive01(Object msg) {
        log.info("QUEUE01接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_fanout02")
    public void receive02(Object msg) {
        log.info("QUEUE02接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_direct01")
    public void receive03(Object msg) {
        log.info("QUEUE01接受消息:" + msg);
    }

    @RabbitListener(queues = "queue_direct02")
    public void receive04(Object msg) {
        log.info("QUEUE02接受消息:" + msg);
    }

    @RabbitListener(queues = "queue_topic01")
    private void receive05(Object msg) {
        log.info("QUEUE01接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_topic02")
    private void receive06(Object msg) {
        log.info("QUEUE02接收消息:" + msg);
    }

    @RabbitListener(queues = "queue_hearder01")
    public void receive07(Message message) {
        log.info("QUEUE01接收Message对象:" + message);
        log.info("QUEUE01接收消息:", new String(message.getBody()));
    }

    @RabbitListener(queues = "queue_hearder02")
    public void receive08(Message message) {
        log.info("QUEUE02接收Message对象:" + message);
        log.info("QUEUE02接收消息:", new String(message.getBody()));
    }
}

4.去Controller中测试

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    MQSender mqSender;
	@RequestMapping("/mq/header01")
    @ResponseBody
    public void mq06() {
        mqSender.send05("hello");
    }

    @RequestMapping("/mq/header02")
    @ResponseBody
    public void mq07() {
        mqSender.send06("hello");
    }
}

项目总结

项目环境搭建

1.SpringBoot环境搭建

2.集成Thymeleaf

3.Mybatis

分布式会话

1.用户登录。明文密码二次MD5加密

2.参数校验+全局异常处理

3.共享Session使用redis实现

功能开发

1.商品列表

2.商品详情

3.秒杀

4.订单详情

系统压测

1.JMeter工具

2.自定义变量模拟多用户

3.压测商品列表和秒杀

优化思路步骤

①缓存页面,将整个页面内容缓存进入redis,但是这样的话每次传输其实还是传输了整个页面。

②页面静态化,优化商品详情页和订单页,因为这两个页面只有一个商品,所以将页面设置为静态的,使用ajax发送请求,然后用jquery设置页面内容。

③将秒杀的商品以及库存放入redis中,先判断redis中的库存是否大于0,如果是就对redis的库存减1。

④当redis中的库存小于0是,后续用户还会查询redis,我们可以用一个HashMap记录库存,对于哈希表中库存小于等于0的,直接返回错误;

⑤将下订单的请求放入RabbitMQ中,让消费者处理,前端用户轮询,达到削峰效果

⑥将使用redis查库存减库存的操作放入lua脚本中,可以保证原子性。

⑦为了防止黄牛获取接口之后使用脚本调用,在秒杀之前,我们先让用户获取接口,在接口中加入UUID并绑定用户和秒杀的商品id,放入redis中。然后用户使用获取的专属接口去秒杀。

⑧为了防止秒杀刚开始请求过多,我们加入验证码,输入正确的验证码后才可以获取接口然后秒杀

⑨使用redis记录用户访问的url,记录30s访问次数,大于5次就提示请求频繁。然后将限流使用注解实现。

解决超卖思路

①首先查询数据库判断商品库存是否小于等于0,小于等于0就返回失败;

②尝试从redis中获取用户相关的订单,key为order:+用户id+":"+商品id,如果redis中不存在,就开始秒杀。

③秒杀时的减库存操作实则时高并发操作,使用sql语句进行减库存

木兰宽松许可证, 第2版 木兰宽松许可证, 第2版 2020年1月 http://license.coscl.org.cn/MulanPSL2 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: 0. 定义 “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 “法人实体”是指提交贡献的机构及其“关联实体”。 “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 1. 授予版权许可 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 2. 授予专利许可 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 3. 无商标许可 “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 4. 分发限制 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 5. 免责声明与责任限制 “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 6. 语言 “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 条款结束 如何将木兰宽松许可证,第2版,应用到您的软件 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; 3, 请将如下声明文本放入每个源文件的头部注释中。 Copyright (c) [Year] [name of copyright holder] [Software Name] is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details. Mulan Permissive Software License,Version 2 Mulan Permissive Software License,Version 2 (Mulan PSL v2) January 2020 http://license.coscl.org.cn/MulanPSL2 Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 0. Definition Software means the program and related documents which are licensed under this License and comprise all Contribution(s). Contribution means the copyrightable work licensed by a particular Contributor under this License. Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. Legal Entity means the entity making a Contribution and all its Affiliates. Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. 1. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. 2. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. 3. No Trademark License No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. 4. Distribution Restriction You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. 5. Disclaimer of Warranty and Limitation of Liability THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 6. Language THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. END OF THE TERMS AND CONDITIONS How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. Copyright (c) [Year] [name of copyright holder] [Software Name] is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details.

简介

商城秒杀项目,附优化思路以及数据库sql文件。烦请各位点点收藏 展开 收起
Java 等 5 种语言
MulanPSL-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/XiangdongHe1/mall-second-kill-project.git
git@gitee.com:XiangdongHe1/mall-second-kill-project.git
XiangdongHe1
mall-second-kill-project
mall-second-kill-project
master

搜索帮助