基于SpringBoot、Redis和RabbitMQ的秒杀系统

基于SpringBoot、Redis和RabbitMQ的秒杀系统

秒杀系统解决的主要问题:并发读、并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。

登录功能总结

一、两次MD5加密

  1. 前端:Password=MD5(明文+固定Salt)
  2. 后端:Password=MD5(用户输入+随机Salt)

用户端MD5加密是为了防止用户密码在网络中明文传输,服务端MD5加密是为了提高密码安全性,双重保险。

  1. 引入pom.xml

    <!-- md5 依赖 -->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>
    
  2. 编写MD5工具类

    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 saltDB) {
            String formPass = inputPassToFormPass(inputPass);
            String dbPass = formPassToDBPass(formPass, saltDB);
            return dbPass;
        }
    
        public static void main(String[] args) {
            System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9
            System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d"));
            System.out.println(inputPassToDbPass("123456", "1a2b3c4d"));
        }
    }
    

二、参数校验

每个类都写大量的健壮性判断过于麻烦,我们可以使用 validation 简化我们的代码

  1. pom.xml

    <!-- validation组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 自定义手机号码验证规则

    /**
     * 手机号码校验规则
     */
    public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {
    
        private boolean required = false;
    
        @Override
        public void initialize(IsMobile constraintAnnotation) {
            required = constraintAnnotation.required();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            if (required){
                return ValidatorUtil.isMobile(value);
            }else {
                if (StringUtils.isEmpty(value)){
                    return true;
                }else {
                    return ValidatorUtil.isMobile(value);
                }
            }
        }
    }
    
  3. 自定义注解IsMobile

    /**
     * 自定义注解验证手机号
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {IsMobileValidator.class})
    public @interface IsMobile {
        boolean required() default true;
        String message() default "手机号码格式错误";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
  4. 校验手机号工具类

    /**
     * 校验手机号工具类
     */
    public class ValidatorUtil {
    
        private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
        public static boolean isMobile(String mobile){
            if (StringUtils.isEmpty(mobile)) {
                return false;
            }
            Matcher matcher = mobile_pattern.matcher(mobile);
            return matcher.matches();
        }
    }
    
  5. 使用方法:直接加在需要校验的字段上

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginVo {
    
        @NotNull
        @IsMobile
        private String mobile;
    
        @NotNull
        @Length(min = 32)
        private String password;
    
    }
    
  6. 登录方法的入参添加@Valid

    /**
    * 登录
    * @return
    */
    @RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin(@Valid LoginVo loginVo) {
       log.info(loginVo.toString());
       return userService.login(loginVo);
    }
    

三、记录用户信息

最常用的就是使用cookie+session记录用户信息

  1. Cookie工具类

    /**
     * Cookie工具类
     */
    public class CookieUtil {
    
        /**
         * 得到Cookie的值, 不编码
         *
         * @param request
         * @param cookieName
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String cookieName) {
            return getCookieValue(request, cookieName, false);
        }
    
        /**
         * 得到Cookie的值,
         *
         * @param request
         * @param cookieName
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
            Cookie[] cookieList = request.getCookies();
            if (cookieList == null || cookieName == null) {
                return null;
            }
            String retValue = null;
            try {
                for (int i = 0; i < cookieList.length; i++) {
                    if (cookieList[i].getName().equals(cookieName)) {
                        if (isDecoder) {
                            retValue = URLDecoder.decode(cookieList[i].getValue(),
                                    "UTF-8");
                        } else {
                            retValue = cookieList[i].getValue();
                        }
                        break;
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return retValue;
        }
    
        /**
         * 得到Cookie的值,
         *
         * @param request
         * @param cookieName
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String
                cookieName, String encodeString) {
            Cookie[] cookieList = request.getCookies();
            if (cookieList == null || cookieName == null) {
                return null;
            }
            String retValue = null;
            try {
                for (int i = 0; i < cookieList.length; i++) {
                    if (cookieList[i].getName().equals(cookieName)) {
                        retValue = URLDecoder.decode(cookieList[i].getValue(),
                                encodeString);
                        break;
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return retValue;
        }
    
        /**
         * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) {
            setCookie(request, response, cookieName, cookieValue, -1);
        }
    
        /**
         * 设置Cookie的值 在指定时间内生效,但不编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) {
            setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
        }
    
        /**
         * 设置Cookie的值 不设置生效时间,但编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) {
            setCookie(request, response, cookieName, cookieValue, -1, isEncode);
        }
    
        /**
         * 设置Cookie的值 在指定时间内生效, 编码参数
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
            doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
        }
    
        /**
         * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
            doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
        }
    
        /**
         * 删除Cookie带cookie域名
         */
        public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
            doSetCookie(request, response, cookieName, "", -1, false);
        }
    
        /**
         * 设置Cookie的值,并使其在指定时间内生效
         *
         * @param cookieMaxage cookie生效的最大秒数
         */
        private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
            try {
                if (cookieValue == null) {
                    cookieValue = "";
                } else if (isEncode) {
                    cookieValue = URLEncoder.encode(cookieValue, "utf-8");
                }
                Cookie cookie = new Cookie(cookieName, cookieValue);
                if (cookieMaxage > 0)
                    cookie.setMaxAge(cookieMaxage);
                if (null != request) {// 设置域名的cookie
                    String domainName = getDomainName(request);
                    System.out.println(domainName);
                    if (!"localhost".equals(domainName)) {
                        cookie.setDomain(domainName);
                    }
                }
                cookie.setPath("/");
                response.addCookie(cookie);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        /**
         * 设置Cookie的值,并使其在指定时间内生效
         *
         * @param cookieMaxage cookie生效的最大秒数
         */
        private static final void doSetCookie(HttpServletRequest request,
                                              HttpServletResponse response,
                                              String cookieName, String cookieValue,
                                              int cookieMaxage, String encodeString) {
            try {
                if (cookieValue == null) {
                    cookieValue = "";
                } else {
                    cookieValue = URLEncoder.encode(cookieValue, encodeString);
                }
                Cookie cookie = new Cookie(cookieName, cookieValue);
                if (cookieMaxage > 0) {
                    cookie.setMaxAge(cookieMaxage);
                }
                if (null != request) {// 设置域名的cookie
                    String domainName = getDomainName(request);
                    System.out.println(domainName);
                    if (!"localhost".equals(domainName)) {
                        cookie.setDomain(domainName);
                    }
                }
                cookie.setPath("/");
                response.addCookie(cookie);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 得到cookie的域名
         */
        private static final String getDomainName(HttpServletRequest request) {
            String domainName = null;
            // 通过request对象获取访问的url地址
            String serverName = request.getRequestURL().toString();
            if (serverName == null || serverName.equals("")) {
                domainName = "";
            } else {
                // 将url地下转换为小写
                serverName = serverName.toLowerCase();
                // 如果url地址是以http://开头 将http://截取
                if (serverName.startsWith("http://")) {
                    serverName = serverName.substring(7);
                }
                int end = serverName.length();
                // 判断url地址是否包含"/"
                if (serverName.contains("/")) {
                    //得到第一个"/"出现的位置
                    end = serverName.indexOf("/");
                }
                // 截取
                serverName = serverName.substring(0, end);
                // 根据"."进行分割
                final String[] domains = serverName.split("\\.");
                int len = domains.length;
                if (len > 3) {
                    // www.xxx.com.cn
                    domainName = domains[len - 3] + "." + domains[len - 2] + "." +
                            domains[len - 1];
                } else if (len <= 3 && len > 1) {
                    // xxx.com or xxx.cn
                    domainName = domains[len - 2] + "." + domains[len - 1];
                } else {
                    domainName = serverName;
                }
            }
            if (domainName != null && domainName.indexOf(":") > 0) {
                String[] ary = domainName.split("\\:");
                domainName = ary[0];
            }
            return domainName;
        }
    }
    
  2. UUID工具类

    /**
     * UUID工具类
     */
    public class UUIDUtil {
        public static String uuid() {
            return UUID.randomUUID().toString().replace("-", "");
        }
    }
    
  3. 登录业务中加入以下代码

    //生成Cookie
    String ticket = UUIDUtil.uuid();
    request.getSession().setAttribute(ticket,user);
    CookieUtil.setCookie(request,response,"userTicket",ticket);
    

四、分布式Session

之前的代码如果所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题,由于 Nginx 使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。也就是说刚开始我们在 Tomcat1 登录之后,用户信息放在 Tomcat1 的 Session 里。过了一会,请求又被 Nginx 分发到了 Tomcat2 上,这时 Tomcat2 上 Session 里还没有用户信息,于是又要登录。

4.1、Spring Session配合Redis实现分布式Session
  1. pom.xml

    <!-- spring data redis 依赖 -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- commons-pool2 对象池依赖 -->
    <dependency>
    	<groupId>org.apache.commons</groupId>
    	<artifactId>commons-pool2</artifactId>
    </dependency>
    <!-- spring-session 依赖 -->
    <dependency>
    	<groupId>org.springframework.session</groupId>
    	<artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
  2. 配置redis,修改application.yml

    spring:
     redis:
       #超时时间
       timeout: 10000ms
       #服务器地址
       host: xxx.xxx.xxx.xxx
       #服务器端口
       port: 6379
       #数据库
       database: 0
       #密码
       password: root
       lettuce:
         pool:
           #最大连接数,默认8
           max-active: 1024
           #最大连接阻塞等待时间,默认-1
           max-wait: 10000ms
           #最大空闲连接
           max-idle: 200
           #最小空闲连接
           min-idle: 5
    
  3. 完成上述操作后进行用户登录会在redis中产生用户信息相关的数据

4.2、直接将用户信息存入Redis
  1. Redis配置类RedisConfig.java

    /**
     * Redis配置类
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
            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(connectionFactory);
            return redisTemplate;
        }
    }
    
  2. 修改之前添加cookie的代码

    //生成Cookie
    String ticket = UUIDUtil.uuid();
    redisTemplate.opsForValue().set("user:" + ticket, user);
    // request.getSession().setAttribute(ticket,user);
    CookieUtil.setCookie(request,response,"userTicket",ticket);
    

此时有一个弊端:用户登录后直接将用户信息存入Redis中了,没有设置过期时间,cookie过期后,重新登录又会产生新的cookie并加到Redis中。

五、优化登录功能

想象一下用户登录后,随意跳转到其它页面是不是都要传入cookie判断用户是否存在,这样重复琐碎的操作可以使用MVC配置类在传入参数前就进行校验。若没有做此层的优化,后续在秒杀功能时,创建的订单将无用户id,原因就是取不到前面传入的用户信息。

  1. UserArgumentResolver.java

    @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.getUserByCookie(ticket, request, response);
        }
    }
    
  2. MVC配置类WebConfig.java

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
    
        @Autowired
        private UserArgumentResolver userArgumentResolver;
    
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(userArgumentResolver);
        }
        
    }
    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值