栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

秒杀商城项目

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

秒杀商城项目

导入依赖

         
             org.springframework.boot
             spring-boot-starter-thymeleaf
         
         
             org.springframework.boot
             spring-boot-starter-web
         
 
         
             mysql
             mysql-connector-java
             runtime
         
         
             org.projectlombok
             lombok
             true
         
         
             org.springframework.boot
             spring-boot-starter-test
             test
         
         
             com.baomidou
             mybatis-plus-boot-starter
             3.3.1.tmp
         
     
配置文件
spring:
 thymeleaf配置
   thymeleaf:
     #关闭缓存
     cache: false
 数据源配置
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
     username: root
     password: hsp
 使用hikari数据源
     hikari:
 连接池名
       pool-name: DateHikariCP
 最小空闲连接数
       minimum-idle: 5
 空闲连接最大存活时间
       idle-timeout: 180000  #30分钟
 最大连接数
       maximum-pool-size: 10
 自动提交连接池返回的数据
       auto-commit: true
       #      连接最大存活时间
       max-lifetime: 180000
 连接超时时间
       connection-timeout: 30000
 测试连接是否可用的查询语句
       connection-test-query: SELECT 1
 
 mybatis-plus:
 mapper.xml映射文件
   mapper-locations: classpath*:/mapper
     @RequestMapping("/hello")
     public String hello(Model model){
         model.addAttribute("name","mgy");
         return "hello";
     }
 }
    前端页面(在templates端口下)

    hello.html

sql表创建
CREATE TABLE t_user(
   `id` BIGINT(20) NOT NULL COMMENT '用户ID,手机号码',
   `nickname` VARCHAR(255) NOT NULL,
   `password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
   `slat` VARCHAR(10) DEFAULT NULL,
   `head` VARCHAR(128) DEFAULT NULL COMMENT '头像',
   `register_date` DATETIME DEFAULT NULL COMMENT '注册时间',
   `last_login_date` DATETIME DEFAULT NULL COMMENT '最后一次登录事件',
   `login_count` INT(11) DEFAULT '0' COMMENT '登录次数',
   PRIMARY KEY(`id`)
   )

两次MD5加密,一次输入密码时加密(前端传给后端),存入数据库时再加密

 
            commons-codec
            commons-codec
        
        
            org.apache.commons
            commons-lang3
            3.6
        
@Component
public class MD5Util {
    public static String md5(String src){
        return DigestUtils.md5Hex(src);
    }
    private static final String salt="1a2b3c4d";
    public static String inputPassToFormPass(String inputPass){
        String str = salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }
    public static String formPassToDBPass(String formPass,String salt){
        String str = salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }
    public static String inputPassToDBPass(String inputPass,String salt){
        String formPass = inputPassToFormPass(inputPass);
        String dbPass = formPassToDBPass(formPass,salt);
        return dbPass;
    }

}
创建逆向工程的模块 规定返回类型

写一个RespBean用于统一返回类型 登录页面

一个方法跳转登录页面登录页面将信息提交到doLogin
RestController=Controller+ResponseBody 手机号码格式校验

@Pattern(regexp = "[1]([3-9])[0-9]{9}$",message = "手机号码格式错误")

@Valid异常捕获

@RestControllerAdvice@ExceptionHandler(Exception.class) 登录信息传递

//生成cookie
        String ticket = UUIDUtil.uuid();
        request.getSession().setAttribute(ticket,user);
        cookieUtil.setcookie(request,response,"userTicket",ticket);
public String toList(HttpSession session, Model model, @cookievalue("userTicket") String ticket){
        //判断ticket是不是空,如果是空,代表没有用户信息,需要去登录
        if(StringUtils.isEmpty(ticket)){
            return "login";
        }
        User user = (User)session.getAttribute(ticket);
        if(user==null){
            return "login";
        }
        //如果user存在,说明已经登录,将用户对象传到前端页面
        model.addAttribute("user",user);
        return "goodsList";
    }
分布式session

            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.apache.commons
            commons-pool2
        
        
            org.springframework.session
            spring-session-data-redis
        

使用redis保存用户登录的信息,就可以实现任何tomcat存放的数据都能被从redis中取出

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;
    }
//生成cookie
        String ticket = UUIDUtil.uuid();
        redisTemplate.opsForValue().set("user:"+ticket,user);

//        request.getSession().setAttribute(ticket,user);
        cookieUtil.setcookie(request,response,"userTicket",ticket);

使用拦截器完善登录功能

用于这个老师教的方法没法没登录就都跳转到login界面,所以配置拦截器实现

@Component
public class AdminInterceptor implements HandlerInterceptor {
    @Autowired
    private IUserService userService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ticket = cookieUtil.getcookievalue(request, "userTicket");
        if(ticket==null){
            response.sendRedirect(request.getContextPath()+"/login/toLogin");
        }
        User user = userService.getUserBycookie(ticket, request, response);
        if(user==null){
            response.sendRedirect(request.getContextPath()+"/login/toLogin");
        }else{
            return true;
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private AdminInterceptor adminInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration = registry.addInterceptor(adminInterceptor);
        registration.addPathPatterns("
    @Override
    public Long getResult(User user, Long goodsId) {
        QueryWrapper wrapper = new QueryWrapper().eq("user_id", user.getId()).eq("goods_id", goodsId);
        SeckillOrder seckillOrder = seckillOrderMapper.selectOne(wrapper);
        if(null != seckillOrder){
            return seckillOrder.getOrderId();
        }else if(redisTemplate.hasKey("isStockEmpty:"+goodsId)){
            //表示库存已经为空,这个人没有抢到
            return -1L;
        }else{
            //表示还在排队
            return 0L;
        }

    }
}

redis的递增递减操作具有原子性 redis锁

一个线程去操作redis就会占位,别的线程就无法操作,这个线程执行完之后删除锁

@Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void testLock01() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //随便设置一个key,每个线程都有去尝试存入这个key,只有这个key不存在的情况下才能存入,返回是否存入成功。
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
        if(isLock){
            valueOperations.set("name","mgy");
            String name = (String) valueOperations.get("name");
            System.out.println(name);
            //一个线程结束后将k1删除,下一个线程才能进来
            redisTemplate.delete("k1");
        }
        else{
            System.out.println("有线程在使用,请稍后");
        }
    }

使用lua脚本能先获取锁,然后判断锁的值是否一致,然后再进行删除(保证每次只删自己的锁,不删别人的锁)

这三个操作在lua脚本中具有原子性 安全优化 隐藏接口地址

先获得一个接口地址,再进行秒杀,根据用户和商品给出唯一的接口地址,让其他人不能使用将以用户和商品为key,地址为value的数据存入Redis

function getSeckillPath(){
        var goodsId = $("#goodsId").val();
        console.log(goodsId);
        g_showLoading();
        $.ajax({
            url:"/seckill/path",
            type:'GET',
            data:{
                goodsId:goodsId,
            },
            success:function(data){
                if(data.code==200){
                    var path = data.obj;
                    doSecKill(path);
                }else{
                    layer.msg(data.message);
                }
            },
            error:function(){
                layer.msg("客户端请求错误");
            }
        })
    }



    function doSecKill(path){
        $.ajax({
            url: '/seckill/'+path+'/doSeckill',
            type: 'POST',
            data:{
                goodsId:$("#goodsId").val(),
            },
            success:function (data){
                if(data.code==200){
                    // window.location.href="/static/orderDetail.htm?orderId="+data.obj.id;
                    getResult($("#goodsId").val());
                }else{
                    layer.msg(data.message);
                }
            },
            error:function (){
                layer.msg("客户端请求错误");
            }
        })
    }
@RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    public RespBean getPath(Long goodsId, HttpServletRequest request, HttpServletResponse response){
        System.out.println("获得路径");
        String ticket = cookieUtil.getcookievalue(request, "userTicket");
        User user = userService.getUserBycookie(ticket,request,response);
        if(user==null){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        String str = orderService.createPath(user,goodsId);
        System.out.println(str);
        return RespBean.success(str);
    }

@Override
    public String createPath(User user, Long goodsId) {
        String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
        //将每个用户随机生成的接口地址存在redis,之后进行校验
        redisTemplate.opsForValue().set("seckillPath:"+user.getId()+":"+goodsId,str,60, TimeUnit.MINUTES);
        return str;
    }
@ResponseBody
    @RequestMapping(value="/{path}/doSeckill",method = RequestMethod.POST)
    public RespBean doSecKill(Long goodsId, @PathVariable String path, HttpServletRequest request, HttpServletResponse response){
        String ticket = cookieUtil.getcookievalue(request, "userTicket");
        User user = userService.getUserBycookie(ticket,request,response);
        if(user==null){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        GoodsVo goods = goodsService.findGoodsByGoodsId(goodsId);
        ValueOperations valueOperations = redisTemplate.opsForValue();
        boolean check = orderService.checkPath(user,goodsId,path);
@Override
    public boolean checkPath(User user, Long goodsId, String path) {
        if(user==null||goodsId<0|| StringUtils.isEmpty(path)){
            return false;
        }
        String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
        return path.equals(redisPath);
    }
添加验证码

增加机器抢购难度减少并发将用户id和商品id做为key,验证码的值作为参数存进Redis里只有输入正确验证码才能通过redis的校验来自gitee上的开源代码,不写了 接口限流

用redis记录次数,有100个缓存之后就无法通过,一分钟到了部分缓存失效,就又可以存入

ValueOperations valueOperations = redisTemplate.opsForValue();
        //获得请求的地址
        String uri = request.getRequestURI();
        //限制访问次数,五秒内访问五次
        Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
        if(count==null){
            //如果还没有这个,就设置一个,此时值为1
            valueOperations.set(uri+":"+user.getId(),1,5,TimeUnit.SECONDS);
        }else if(count<5){
            //如果值小于5,就递增
            valueOperations.increment(uri+":"+user.getId());
        }else{
            return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
        }

限流控制在最大能承受的QPS的70%-80%这个方法的问题在于临界失效前后如果有大量请求的话,还是会超过QPS漏桶算法:一部分进一部分出,通常用队列实现,但是可能导致桶被装满,太少会造成资源的浪费令牌算法:以恒定的速度生成令牌,放进令牌桶里,如果桶满了就丢弃令牌,一个请求出现后要去拿令牌,拿到令牌才能够被执行[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TIsm9Hf-1648091808661)(.doc.markdown_images/9d58a6b5.png)]redis优势:具有递增原子性,能够设计失效时间 通用接口限流

threadLocal:每个线程绑定自己的值,不会造成用户信息紊乱。相当于每个线程中一个存放私有数据的盒子

@Retention(RetentionPolicy.RUNTIME)//在运行时使用
@Target(ElementType.METHOD)//在方法上使用
public @interface AccessLimit {

    int second();

    int maxCount();

    boolean needLogin() default true;
}
public class AccessLimitInterceptor implements HandlerInterceptor {
    @Autowired
    private IUserService userService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取用户
        String ticket = cookieUtil.getcookievalue(request, "userTicket");
        User user = userService.getUserBycookie(ticket,request,response);
        UserContext.setUser(user);

        //      判断拦截的是不是个方法
        if(handler instanceof HandlerMethod){
            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();
            String key = request.getRequestURI();
            if(needLogin){
                if(user==null){
                    return false;
                }
                key+=":"+user.getId();
            }
            ValueOperations valueOperations = redisTemplate.opsForValue();
            Integer count = (Integer) valueOperations.get(key);
            if(count==null){
                valueOperations.set(key,1,second, TimeUnit.SECONDS);
            }else if(count 
秒杀主流方案分析 
需要注意的问题 

高并发,刷接口等黑客请求,超出负载高并发时导致的超卖该负载情况下下单成功率的保障 网关限流

黑名单,放在内存里多次请求,redis缓存重复没有预约(没有令牌)预约可以提前发放部分令牌redission加分布式锁,redis集群

AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            //如果没有这个注解
            if(accessLimit==null){
                return true;
            }
            int second = accessLimit.second();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();
            String key = request.getRequestURI();
            if(needLogin){
                if(user==null){
                    return false;
                }
                key+=":"+user.getId();
            }
            ValueOperations valueOperations = redisTemplate.opsForValue();
            Integer count = (Integer) valueOperations.get(key);
            if(count==null){
                valueOperations.set(key,1,second, TimeUnit.SECONDS);
            }else if(count 
秒杀主流方案分析 
需要注意的问题 

高并发,刷接口等黑客请求,超出负载高并发时导致的超卖该负载情况下下单成功率的保障 网关限流

黑名单,放在内存里多次请求,redis缓存重复没有预约(没有令牌)预约可以提前发放部分令牌redission加分布式锁,redis集群

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/778050.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号