秒杀系统解决的主要问题:并发读、并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。
用户端MD5加密是为了防止用户密码在网络中明文传输,服务端MD5加密是为了提高密码安全性,双重保险。
引入pom.xml
<!-- md5 依赖 --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency>
编写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
简化我们的代码
pom.xml
<!-- validation组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
自定义手机号码验证规则
/** * 手机号码校验规则 */ 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); } } } }
自定义注解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 {}; }
校验手机号工具类
/** * 校验手机号工具类 */ 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(); } }
使用方法:直接加在需要校验的字段上
@Data @NoArgsConstructor @AllArgsConstructor public class LoginVo { @NotNull @IsMobile private String mobile; @NotNull @Length(min = 32) private String password; }
登录方法的入参添加@Valid
/** * 登录 * @return */ @RequestMapping("/doLogin") @ResponseBody public RespBean doLogin(@Valid LoginVo loginVo) { log.info(loginVo.toString()); return userService.login(loginVo); }
最常用的就是使用cookie+session记录用户信息
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; } }
UUID工具类
/** * UUID工具类 */ public class UUIDUtil { public static String uuid() { return UUID.randomUUID().toString().replace("-", ""); } }
登录业务中加入以下代码
//生成Cookie String ticket = UUIDUtil.uuid(); request.getSession().setAttribute(ticket,user); CookieUtil.setCookie(request,response,"userTicket",ticket);
之前的代码如果所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题,由于 Nginx 使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。也就是说刚开始我们在 Tomcat1 登录之后,用户信息放在 Tomcat1 的 Session 里。过了一会,请求又被 Nginx 分发到了 Tomcat2 上,这时 Tomcat2 上 Session 里还没有用户信息,于是又要登录。
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>
配置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
完成上述操作后进行用户登录会在redis中产生用户信息相关的数据
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; } }
修改之前添加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,原因就是取不到前面传入的用户信息。
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); } }
MVC配置类WebConfig.java
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Autowired private UserArgumentResolver userArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(userArgumentResolver); } }