秒杀功能(1)登陆功能

一直懒于做笔记,今天突然想好好把项目记录下来,希望秋招时能收到满意的offer(实习计划已经凉透了)。不过发现记笔记也太耗时了,也不知道自己能坚持多久。先把之前做的都梳理一下吧。

做了一个秒杀系统(系统算不上,勉强称为功能吧),毕竟不在大厂实习过也没有高并发的经历(何止大厂,小厂都没有,扎心了),想体验一下秒杀的场景和高并发时发生的问题(呵,谁没事想体验,还不是被面试逼的)。

第一部分:登陆功能

  1. 数据库设计;
  2. 明文密码两次MD5加密;
    • 用户端(明文密码+固定salt)
    • 服务端(将用户端传来的密码+随机salt)
  3. Redis+Session管理;
  4. 参数校验+全局异常处理。

1. 数据库设计与基础层

这里直接给entity的代码了,数据库和entity的字段一一对应。

public class MiaoshaUser {
    private Long id;//手机号
    private String nickname;
    private String password;
    private String salt;
    private String head;//头像
    private Date registerDate;
    private Date lastLoginDate;
    private Integer loginCount;
    //Getter and Setter
    //...
}

给出Dao层代码:

@Repository//这里一定要有这个注解,否则会报错,为什么?
@Mapper
public interface MiaoshaUserDao {

    @Select("select * from user where id = #{id}")
    public MiaoshaUser getById(@Param("id") long id);
}

2. 两次MD5加密

用户输入密码到请求中,做了一次MD5加密,此时salt是固定值;从请求到数据库中又做了一次MD5加密,这个MD5是随机生成并存在数据库中的。

public class MD5Util {

	//这里直接添加maven依赖、导包、用了库函数
    public static String md5(String src) {
        return DigestUtils.md5Hex(src);
    }

	//第一次加密的固定salt
    private static final String salt = "1a2b3c4d";

    //从客户输入到网上,第一次md5
    public static String inputPassToFormPass(String inputPass) {
        String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);//这里排列组合可以任意,记住即可
        return md5(str);
    }

    //从请求到数据库,第二次md5
    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("12222222222"));
		System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d"));
		System.out.println(inputPassToDbPass("12222222222", "123456"));
    }

}

3. Redis+Session管理

Redis不介绍了,这里主要记一下其在项目中是如何用的。业务中将token的键值对存放在redis中。每次登陆,都更新用户的token和redis的过期时间。

1. Redis的Key生成
Redis的key生成也是一个比较关键的部分,这里采用接口+抽象类+具体实现的形式将Redis的key生成出来。

先写一个接口。

public interface KeyPrefix {

    public int expireSeconds();//过期时间

    public String getPrefix();

}

抽象类。

public abstract class BasePrefix implements KeyPrefix {

    private int expireSeconds;

    private String prefix;

    public BasePrefix(String prefix) {//0代表永不过期
        this(0, prefix);
    }

    public BasePrefix(int expireSeconds, String prefix) {
        this.expireSeconds = expireSeconds;
        this.prefix = prefix;
    }

    public int expireSeconds() {//默认0代表永不过期
        return expireSeconds;
    }

    public String getPrefix() {
        String className = getClass().getSimpleName();//得到类的简写名称
        return className + ":" + prefix;
    }
}

实现类。

public class MiaoshaUserKey extends BasePrefix {

    public static final int TOKEN_EXPIRE = 3600 * 24 * 2; //假设2天

    private MiaoshaUserKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }

    public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}

2. Redis的配置和RedisService
先在properties中添加配置。

#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.timeout=3000ms
spring.redis.jedis.pool.max-wait=3000ms

写一个RedisService的配置类/包装类。

@Service
public class RedisService implements InitializingBean {

    private JedisPool jedisPool;

    @Override
    public void afterPropertiesSet() throws Exception { //生成bean时执行的方法
        jedisPool = new JedisPool();
    }
    /**
     * 获取单个对象
     */
    public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            String str = jedis.get(realKey);
            T t = stringToBean(str, clazz);
            return t;
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 设置对象
     */
    public <T> boolean set(KeyPrefix prefix, String key, T value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = beanToString(value);
            if (str == null || str.length() <= 0) {
                return false;
            }
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            int seconds = prefix.expireSeconds();
            if (seconds <= 0) {
                jedis.set(realKey, str);
            } else {
                jedis.setex(realKey, seconds, str);
            }
            return true;
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 判断key是否存在
     */
    public <T> boolean exists(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.exists(realKey);
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 增加值
     */
    public <T> Long incr(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.incr(realKey);
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 减少值
     */
    public <T> Long decr(KeyPrefix prefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //生成真正的key
            String realKey = prefix.getPrefix() + key;
            return jedis.decr(realKey);
        } finally {
            returnToPool(jedis);
        }
    }

    private <T> String beanToString(T value) {
        if (value == null) {
            return null;
        }
        Class<?> clazz = value.getClass();
        if (clazz == int.class || clazz == Integer.class) {
            return "" + value;
        } else if (clazz == String.class) {
            return (String) value;
        } else if (clazz == long.class || clazz == Long.class) {
            return "" + value;
        } else {
            return JSON.toJSONString(value);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T stringToBean(String str, Class<T> clazz) {
        if (str == null || str.length() <= 0 || clazz == null) {
            return null;
        }
        if (clazz == int.class || clazz == Integer.class) {
            return (T) Integer.valueOf(str);
        } else if (clazz == String.class) {
            return (T) str;
        } else if (clazz == long.class || clazz == Long.class) {
            return (T) Long.valueOf(str);
        } else {
            return JSON.toJavaObject(JSON.parseObject(str), clazz);
        }
    }

    private void returnToPool(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

}

3. Service层关于Session的内容

@Service
public class MiaoshaUserService {

    public static final String COOKI_NAME_TOKEN = "token";

    @Autowired
    MiaoshaUserDao miaoshaUserDao;

    @Autowired
    RedisService redisService;

    public MiaoshaUser getById(long id) {
        return miaoshaUserDao.getById(id);
    }

    public MiaoshaUser getByToken(HttpServletResponse response, String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
        //延长有效期
        if (user != null) {
            addCookie(response, token, user);
        }
        return user;
    }


    public String login(HttpServletResponse response, LoginVo loginVo) {
        //一系列验证操作,先省略
        //...
		//验证成功后,最后生成cookie
        String token = UUIDUtil.uuid();
        addCookie(response, token, user);
        return token;
    }

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
        redisService.set(MiaoshaUserKey.token, token, user);//把token信息写到缓存中,在redis中管理session
        Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);//在cookie中放入名为“token” 值为token的字段
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());//这里把cookie和token的Redis设为一致的有效期
        cookie.setPath("/");
        response.addCookie(cookie);
    }
}

相关代码块:UUIDUtil和LoginVo(LoginVo在后面贴,涉及到下一个知识点)

public class UUIDUtil {
    public static String uuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

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

这个模块的功能主要是避免过多重复复杂的判断,为了表现出代码的简洁性,把参数校验和业务代码抽离出来,并采用全局异常处理的形式统一处理异常。

先给出两个辅助类的代码,Result类和CodeMsg类,这两个类的作用分别是返回结果和错误代码标注。

public class Result<T> {

    private int code;
    private String msg;
    private T data;

    /**
     * 成功时候的调用
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>(data);
    }

    /**
     * 失败时候的调用
     */
    public static <T> Result<T> error(CodeMsg codeMsg) {
        return new Result<T>(codeMsg);
    }
	
	//三种构造函数
    private Result(T data) {
        this.data = data;
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Result(CodeMsg codeMsg) {
        if (codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
    //Getter and Setter...
}

public class CodeMsg {

    private int code;
    private String msg;

    //通用的错误码
    public static CodeMsg SUCCESS = new CodeMsg(0, "success");
    public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
    public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
    //登录模块 5002XX
    public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
    public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
    public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
    public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
    public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
    public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");

    //商品模块 5003XX

    //订单模块 5004XX

    //秒杀模块 5005XX
    public static CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
    public static CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");

	//两种构造函数
    private CodeMsg() {}

    private CodeMsg(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
	
	//Getter and Setter...

    public CodeMsg fillArgs(Object... args) {//变参数,用args填充message
        int code = this.code;
        String message = String.format(this.msg, args);
        return new CodeMsg(code, message);
    }

    @Override
    public String toString() {
        return "CodeMsg [code=" + code + ", msg=" + msg + "]";
    }
}
  • 参数校验部分
    先从controller层代码讲起。
@Controller
@RequestMapping("/login")
public class LoginController {

    private static Logger log = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    MiaoshaUserService miaoshaUserService;

    @Autowired
    RedisService redisService;
    
    //请求登陆页面的url,返回登陆页面
    @RequestMapping("/to_login") 
    public String toLogin() {
        return "login";
    }
	
    @RequestMapping("/do_login")
    @ResponseBody //返回jason串
    public Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {//注意这边有个valid,那么这个类里面需要设置一系列注解
        log.info(loginVo.toString());
        //登录
        String token = miaoshaUserService.login(response, loginVo);
        return Result.success(token);
    }
}

注意到这里有个@Valid参数校验的注解,表示对loginVo这个对象进行参数校验。贴上LoginVo类的代码。

public class LoginVo {

    @NotNull//表示不能为空
    @IsMobile//自定义一个校验器
    private String mobile;

    @NotNull
    @Length(min = 32)//表示长度最小为32位
    private String password;

	//Getter and Setter...

    @Override
    public String toString() {
        return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
    }
}

这里两个字段上均有注解,表示mobile不能为空且要符合自定义注解@IsMobile的要求,password字段需要不能为空且长度最少32位。

下面看一下如何自定义@IsMobile注解。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@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 {};
}

这里出现了IsMobileValidator.class,这是具体的校验方法,贴上代码。

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    //初始化
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    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);
            }
        }
    }
}

里面用到了辅助工具类ValidatorUtil,这是具体的方法,当然也可以不用这个类直接写在上面代码中,这里为了清晰把具体判别方法写成辅助类。

public class ValidatorUtil {

    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");//正则表达式,以1开头,后面跟了10个数字

    public static boolean isMobile(String src) {
        if (StringUtils.isEmpty(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);//匹配
        return m.matches();
    }
}

参数校验部分就全部完成啦~

  • 全局异常处理部分
    自顶往下看吧,先完善Service层的代码。
@Service
public class MiaoshaUserService {

    public static final String COOKI_NAME_TOKEN = "token";

    @Autowired
    MiaoshaUserDao miaoshaUserDao;

    @Autowired
    RedisService redisService;

    public MiaoshaUser getById(long id) {...}//上面实现过了,略

    public MiaoshaUser getByToken(HttpServletResponse response, String token) {...}//已写,略。

    public String login(HttpServletResponse response, LoginVo loginVo) {
        if (loginVo == null) {
            throw new GlobalException(CodeMsg.SERVER_ERROR);//直接抛,在handler中会集中处理
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = getById(Long.parseLong(mobile));
        if (user == null) {
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String saltDB = user.getSalt();
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
        if (!calcPass.equals(dbPass)) {
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        //生成cookie
        String token = UUIDUtil.uuid();
        addCookie(response, token, user);
        return token;
    }

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {...} //略。
}

这里面统一抛了GlobalException,其实现为:

public class GlobalException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private CodeMsg cm;

    public GlobalException(CodeMsg cm) {
        super(cm.toString());
        this.cm = cm;
    }

    public CodeMsg getCm() {
        return cm;
    }
}

这只是继承了RuntimeException,重新包装了一下,下面才是重点,捕捉全局异常~

@ControllerAdvice//@ControllerAdvice是一个@Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。
@ResponseBody
public class GlobalExceptionHandler {
    @ExceptionHandler(value = Exception.class)//拦截什么类型的异常,这里是拦截所有异常
    public Result<String> exceptionHandler(HttpServletRequest request, Exception e) {
        e.printStackTrace();
        if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return Result.error(ex.getCm());
        } else if (e instanceof BindException) { //参数验证异常,上面定义的@Valid参数不合法时
            BindException ex = (BindException) e;
            List<ObjectError> errors = ex.getAllErrors();//也许有多个错误
            ObjectError error = errors.get(0);//获取第一个错误
            String msg = error.getDefaultMessage();
            return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
        } else {
            return Result.error(CodeMsg.SERVER_ERROR);
        }
    }
}

第一部分终于完成啦,写了一天了,哭泣,第一次用markdown,感觉有点好玩~ 明天再更~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值