• 从零开始学习秒杀项目


            构思了很多种讲述这个简易版的秒杀项目的思路,比如按照功能分类,按照项目亮点串起来讲述,总觉得不适合基础薄弱的同学来学习,所以本项目按照从搭建开始,过程中需要什么来学习什么。

    技术栈

    SpringBoot+mybatisPlus,MySQL,Redis,RabbitMQ

    项目地址

    ma/seckill

    亮点

    1.自定义注解(Spring AOP)

    2.用户密码两次MD5加密

            第一次MD5加密:防止用户明文密码在网络进行传输

            第二次MD5加密:防止数据库被盗,避免通过MD5反推出密码,双重保险

    3.redis分布式锁保证秒杀业务场景下的正确性

    4.秒杀操作后的秒杀成功信息进入RabbitMQ进行排队

     项目代码结构图

    准备

    创建SpringBoot项目,写入Maven文件,导入sql文件。

    数据库介绍

    使用MYSQL

    goods代表货物信息表

    order_info订单详情表

    seckill_good秒杀商品表

    seckill_order秒杀商品订单表

    user用户表

    Redis服务启动

    这里使用的是Redis-windows版本,无密码

    RabbitMQ服务启动

    如果你不会安装RabbitMQ,请查看windows环境下安装RabbitMQ(超详细)_windows安装rabbitmq-CSDN博客

    http://localhost:15672/#/保证本机RabbitMQ服务启动(账号是默认账号guest/guest)

    http://localhost:15672/#/

    Coding

    用户登录模块

    对应LoginController.java

    一共有两个方法,一个是界面跳转方法,略过

    1. @RequestMapping("/do_login")
    2. @ResponseBody
    3. public Result doLogin(@Valid LoginParam loginParam, HttpServletResponse response){
    4. System.out.println(loginParam);
    5. //登陆
    6. String token = userService.login(response, loginParam);
    7. return Result.success(token);
    8. }

    首先来说一下整个方法的返回值Result.java

    1. public class Result {
    2. private int code;
    3. private String msg;
    4. private T data;
    5. private Result(T data) {
    6. this.code = CodeMsg.SUCCESS.getCode();
    7. this.msg = CodeMsg.SUCCESS.getMsg();
    8. this.data = data;
    9. }
    10. public boolean isSuccess(){
    11. return this.code==CodeMsg.SUCCESS.getCode();
    12. }
    13. public static Result success(T data){
    14. return new Result(data);
    15. }
    16. public static Result error(CodeMsg codeMsg){
    17. return new Result(codeMsg);
    18. }
    19. private Result(int code, String msg) {
    20. this.code = code;
    21. this.msg = msg;
    22. }
    23. private Result(CodeMsg codeMsg) {
    24. if(codeMsg != null) {
    25. this.code = codeMsg.getCode();
    26. this.msg = codeMsg.getMsg();
    27. }
    28. }
    29. public int getCode() {
    30. return code;
    31. }
    32. public void setCode(int code) {
    33. this.code = code;
    34. }
    35. public String getMsg() {
    36. return msg;
    37. }
    38. public void setMsg(String msg) {
    39. this.msg = msg;
    40. }
    41. public T getData() {
    42. return data;
    43. }
    44. public void setData(T data) {
    45. this.data = data;
    46. }
    47. }

    里面有三个变量,分别代表着结果代码,结果信息,结果中返回的数据,T代表泛型的意思,不懂的可以百度;CodeMsg代表的具体定义的一些成功和异常信息。

    接下来我们再返回LoginController里面的doLogin方法,可以注意到在函数参数中使用了@Valid注解,代表着LoginParam需要进行参数检查,我们进入LoginParam.java

    1. import com.lgc.SeckillProject.vaildator.IsMobile;
    2. import lombok.Getter;
    3. import lombok.Setter;
    4. import lombok.ToString;
    5. import org.hibernate.validator.constraints.Length;
    6. import javax.validation.constraints.NotNull;
    7. @Getter
    8. @Setter
    9. @ToString
    10. public class LoginParam {
    11. @NotNull(message = "手机号不能为空")
    12. @IsMobile
    13. private String mobile;
    14. @NotNull
    15. @Length(min = 23,message = "密码长度需要在7个字以内")
    16. private String password;
    17. }

    上面的代码特意粘贴了使用注解来源于哪个包,明确一下注解的来源。

    代码中的两个字段mobile和password两个字段分别用@NotNull注解修饰,意思是两个字段传入的时候不允许为空。

    另外password对字段的长度进行了限制.

    拓展:

    我们还注意到他使用了@IsMobile注解,这是一个自定义的注解,也是本项目中的一个亮点。

    我们需要自定义注解,首先创建一个自定义注解类

    1. @Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER}) //注解的作用范围 :方法,字段、枚举的常量,注解,构造函数,方法参数
    2. @Retention(RetentionPolicy.RUNTIME) //注解的生命周期:默认是class,RUNTIME:运行时存在。RUNTIME>class>source
    3. @Documented //如果一个注解@B,被@Documented标注,那么被@B修饰的类,生成文档时,会显示@B。如果@B没有被@Documented标准,最终生成的文档中就不会显示@B。
    4. @Constraint(validatedBy = {IsMobileValidator.class})//自定义约束
    5. public @interface IsMobile {
    6. boolean required() default true;
    7. String message() default "手机号码格式错误";
    8. Class[] groups() default {};
    9. Classextends Payload>[] payload() default {};
    10. }

    从上面看,有很多陌生的注解,我们一个一个来分析

    首先是@Target注解,里面写入了这个注解的使用范围,包括可以在方法上使用,字段、枚举的常量,注解,构造函数,方法参数

    @Retention(RetentionPolicy.RUNTIME)注解的生命周期,一般是RUNTIME,默认是class;RUNTIME>CLASS>SOURCE

    @Document .如果一个注解@B,被@Documented标注,那么被@B修饰的类,生成文档时,会显示@B。如果@B没有被@Documented标准,最终生成的文档中就不会显示@B

    @Constraint 自定义约束,IsMobileValidator.class是自定义的约束类

    自定义注解中有几个方法,并且每个方法中都有默认的值。

    接下来我们来看一下注解中自定义约束类IsMobileValidator.java

    1. public class IsMobileValidator implements ConstraintValidator {
    2. private boolean required=false;
    3. @Override
    4. public void initialize(IsMobile constraintAnnotation) {
    5. required=constraintAnnotation.required();
    6. }
    7. @Override
    8. public boolean isValid(String value, ConstraintValidatorContext context) {
    9. if (required){
    10. return VaildatorUtil.isMobile(value);
    11. }else{
    12. if (StringUtils.isEmpty(value)){
    13. return true;
    14. }else{
    15. return VaildatorUtil.isMobile(value);
    16. }
    17. }
    18. }
    19. }

    首先实现接口ConstraintValidator,参数为自定义注解和String

    重写里面的initialize方法还有isValid方法,require代表的是需不需要进行验证,isValid是验证方法,如果是需要的验证的话,调用isValid方法,isValid里面又使用ValidatorUtil.isMobild方法,这个方法的内部是使用正则表达式进行实现的。

    至此,自定义注解讲解完成。

    我们返回LoginController继续,我们来看一下userService.login这个方法,进入UserServiceImpl中login方法。

    我们来分析一下上面的红框中的代码,为了防止从浏览器中输入的密码在网络中明文传输,我们使用了MD5进行加密,在从浏览器的输入中获取密码后加入“盐”使用MD5.formPassToDBPass进行加密处理,然后再与数据库中已加密的密码进行比较。

    以上是加密的细节。

    返回UserServiceImpl,继续看login方法后半段生成cookie部分

    使用UUIDUtil工具类生成随机的token ,进入addCookie方法

    1. //生成cookie
    2. String token = UUIDUtil.uuid();
    3. addCookie(response,token,user);
    4. return token;

    在Cookie方法中,把生成的token作为key的后半段,UserKey.token作为前半段,拼接构成key值,user对象作为value值传入Redis,并且生成一个Cookie对象放入response中。

    1. private void addCookie(HttpServletResponse response,String token,User user){
    2. redisService.set(UserKey.token,token,user,UserKey.TOKEN_EXPIRE);
    3. Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
    4. cookie.setMaxAge(UserKey.TOKEN_EXPIRE);
    5. cookie.setPath("/");
    6. response.addCookie(cookie);
    7. }

    这里重点讲解一下redisService.set方法的内部逻辑

    1. /**
    2. * 设置对象
    3. *
    4. * @param prefix 对象Prefix
    5. * @param key 键
    6. * @param value 值
    7. * @param exTime 过期时间
    8. * @param 返回类型
    9. * @return
    10. */
    11. public boolean set(KeyPrefix prefix,String key,T value,int exTime){
    12. String str = beanToString(value);
    13. if (str==null || str.length() <= 0){
    14. return false;
    15. }
    16. //生成唯一key
    17. String realKey = prefix.getPrefix() + key;
    18. //设置过期时间
    19. if (exTime<=0){
    20. stringRedisTemplate.opsForValue().set(realKey,str);
    21. }else{
    22. return stringRedisTemplate.opsForValue().setIfAbsent(realKey,str,exTime, TimeUnit.SECONDS);
    23. }
    24. return true;
    25. }

    为了保证整个秒杀业务中商品数量的正确性,关键的一个步骤是并发场景下,锁的竞争,在这里使用了redis的分布式锁机制,也就是setIfAbsent这个方法,我认为也是整个项目的关键之一。在本文中的最后部分《redis分布式锁解析》详细介绍一下这个东西。

    订单模块

    对应orderController.java

    订单模块的控制层只包含一个方法,info 方法,参数为user以及订单号,返回值为订单详情。

    该模块大多数为增删改查以及简单的业务逻辑,较为简单,自己顺着代码看看就可以

    货物模块

    对应GoodsController.java

    首先针对list方法进行分析。

    为了应对在SpringBoot中的高并发及优化访问速度,我们一般会把页面上的数据查询出来,然后放到redis中进行缓存,减少数据库的压力。如果再进行改进的话,可以对整个界面进行缓存。

    1. @RequestMapping("/list")
    2. @ResponseBody
    3. public String list(Model model, HttpServletRequest request, HttpServletResponse response){
    4. //取缓存
    5. String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
    6. if (!StringUtils.isEmpty(html)){
    7. return html;
    8. }
    9. //获取数据绑定到model
    10. List goodsVos = goodService.listGoodVo();
    11. model.addAttribute("goodsVos",goodsVos);
    12. WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
    13. //手动渲染
    14. html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
    15. if (!StringUtils.isEmpty(html)){
    16. redisService.set(GoodsKey.getGoodsList,"",html,60);
    17. }
    18. return html;
    19. }

    goodsDetail和detailStatic思路也是一样的,无非就是添加了秒杀状态还有倒计时这俩,思路和上面的代码是一致的。自己顺着代码理一遍就可以

    秒杀模块

    对应模块SeckillController.java

    实现了InitializingBean接口,需要重写afterPropertiesSet方法,系统启动后,进行初始化,将热点数据放入redis中。

    这里在方法里还定义了一个map,我们知道map的存储位置是内存中,所以我们将秒杀的商品的Id放入内存中,查询速度会飞快。

    下面介绍一下秒杀接口1.0以及改进版本,从一开始的QPS793经过优化后QPS1658

    首先是1.0版本    QPS:793 * 线程:5000 * 10 * 进行秒杀

    1. @RequestMapping("/do_seckill")
    2. public String seckill(Model model, User user, @RequestParam("goodsId")long goodsId){
    3. model.addAttribute("user",user);
    4. if (user==null){
    5. return "login";
    6. }
    7. //库存判断
    8. GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
    9. int stock = goodsVo.getStockCount();
    10. if (stock<=0){
    11. model.addAttribute("ErrorMsg", CodeMsg.MIAO_SHA_OVER.getMsg());
    12. return "seckill_fail";
    13. }
    14. //判断是否秒杀到了
    15. SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
    16. if (order!=null){
    17. model.addAttribute("ErrorMsg",CodeMsg.REPEATE_MIAOSHA.getMsg());
    18. return "seckill_fail";
    19. }
    20. //减库存下订单,写入秒杀订单
    21. OrderInfo orderInfo = seckillService.seckill(user, goodsVo);
    22. model.addAttribute("orderInfo",orderInfo);
    23. model.addAttribute("goods",goodsVo);
    24. return "order_detail";
    25. }

    核心是将处理好的数据放到model,界面查询速度偏慢

    2.0版本    * QPS:1206 * 线程:5000 * 10 * 订单页面静态化

    1. @RequestMapping(value = "/seckill",method = RequestMethod.POST)
    2. @ResponseBody
    3. public Result seckillStatic(User user,@RequestParam("goodsId")long goodsId){
    4. if (user==null){
    5. return Result.error(CodeMsg.SESSION_ERROR);
    6. }
    7. //判断库存
    8. GoodsVo goods = goodsService.getGoodsVoById(goodsId);
    9. int stock=goods.getStockCount();
    10. if (stock<=0){
    11. return Result.error(CodeMsg.MIAO_SHA_OVER);
    12. }
    13. //判断是否已经秒杀到了
    14. SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
    15. if (order!=null){
    16. return Result.error(CodeMsg.REPEATE_MIAOSHA);
    17. }
    18. //减少库存,写入秒杀订单
    19. OrderInfo orderInfo = seckillService.seckill(user, goods);
    20. return Result.success(orderInfo);
    21. }

    整体的流程和1.0没有区别,区别在于将最后的结果封装在Result中了。速度还可以再提升

    3.0版 * QPS:1658 * 线程:5000 * 10 * 加入消息队列

    1. @RequestMapping(value = "/{path}/seckill_mq",method = RequestMethod.POST)
    2. @ResponseBody
    3. public Result seckillMq(User user, @RequestParam("goodsId")long goodsId, @PathVariable("path")String path){
    4. if (user==null){
    5. return Result.error(CodeMsg.SESSION_ERROR);
    6. }
    7. //验证path
    8. boolean checkPath = seckillService.checkPath(user, goodsId, path);
    9. if (!checkPath){
    10. return Result.error(CodeMsg.REQUEST_ILLEGAL);
    11. }
    12. //内存标记,减少Redis访问
    13. Boolean over = localOverMap.get(goodsId);
    14. if (over){
    15. return Result.error(CodeMsg.MIAO_SHA_OVER);
    16. }
    17. //预减库存
    18. Long stock = redisService.decr(GoodsKey.getSeckillGoodStock, "" + goodsId);
    19. if (stock<0){
    20. localOverMap.put(goodsId,true);
    21. return Result.error(CodeMsg.MIAO_SHA_OVER);
    22. }
    23. //判断是否秒杀到了
    24. SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
    25. if (order!=null){
    26. return Result.error(CodeMsg.REPEATE_MIAOSHA);
    27. }
    28. //压入消息队列中
    29. //入队
    30. SeckillMessage sm = new SeckillMessage();
    31. sm.setUser(user);
    32. sm.setGoodsId(goodsId);
    33. sender.sendSeckillMessage(sm);
    34. return Result.success(0);//排队中
    35. }

    3.0版本一个是引入了本地map减少了Redis的访问,另外使用了预减库存,秒杀成功后将秒杀成功信息发送到RabbitMQ中,进入队列中排队。

    从代码中可以看出预减成功后也只是进入到了RabbitMQ中进行排队,不至于阻塞,至于最后入库,还需要对队列进行监听。

    值得说一点的是,秒杀接口操作的层次仅仅只在Redis中,所有操作的数据都在Redis中,所以此过程不存在与数据库的任何操作,也就是说你如果在秒杀过程中失败了,不会影响到数据库中的数据,这是极为巧妙的,也是值得学习的,只有当秒杀成功后,秒杀成功的消息放入MQ,并且MQ监听到的时候,此时监听到的信息才会真正的创建订单并存入数据库。

    1. @RabbitListener(queues = MQConfig.SECKILL_QUEUE)
    2. public void receive(String message){
    3. log.info("receive message:"+message);
    4. SeckillMessage sm = redisService.stringToBean(message, SeckillMessage.class);
    5. User user = sm.getUser();
    6. long goodsId = sm.getGoodsId();
    7. GoodsVo goods = goodsService.getGoodsVoById(goodsId);
    8. int stock=goods.getStockCount();
    9. if (stock<=0){
    10. return;
    11. }
    12. //判断是否秒杀到了
    13. SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
    14. if (order!=null){
    15. return;
    16. }
    17. //减库存 下订单 写入秒杀订单
    18. seckillService.seckill(user,goods);
    19. }

    上面的方法是RabbitMQ监听队列SECKILL_QUEUE,按照顺序,从队列中读取数据同步数据库操作。

    1. /**
    2. * 客户端轮询查询是否下单成功
    3. * orderId:成功
    4. * -1:秒杀失败
    5. * 0: 排队中
    6. */
    7. @RequestMapping(value = "/result",method = RequestMethod.GET)
    8. @ResponseBody
    9. public Result seckillResult(@RequestParam("goodsId")long goodsId,User user){
    10. if (user==null){
    11. return Result.error(CodeMsg.USER_NO_LOGIN);
    12. }
    13. long result = seckillService.getSeckillResult(user.getId(), goodsId);
    14. return Result.success(result);
    15. }

    前端轮训查询是否下单成功,最终查询的是数据库中的数据,而不是Redis中的数据。

    1. /**
    2. * 获取秒杀地址
    3. * 自定义接口限流:5秒内最多访问5次,并需要为登录状态
    4. * @param user
    5. * @param goodsId
    6. * @return
    7. */
    8. @AccessLimit(seconds = 5,maxCount = 5,needLogin = true)
    9. @RequestMapping(value = "/path",method = RequestMethod.GET)
    10. @ResponseBody
    11. public Result getSeckillPath(User user,@RequestParam("goodsId")long goodsId){
    12. if (user==null){
    13. return Result.error(CodeMsg.USER_NO_LOGIN);
    14. }
    15. String path = seckillService.createPath(user, goodsId);
    16. return Result.success(path);
    17. }

    上面是获取秒杀地址的接口,因为主要的并发压力在这个接口,所以需要对这个接口进行限流。

    核心点在@AccessLimit这个自定义注解中,来看一下自定义注解的定义

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.METHOD)
    3. public @interface AccessLimit {
    4. int seconds();
    5. int maxCount();
    6. boolean needLogin() default true;
    7. }

    注解不解释了,里面定义了三个方法,seconds(),maxCount(),needLogin(),除了第三个方法从字面意思上能知道是干啥用的,其余头俩都不清楚,这就引出了Spring一个很重要的特性,面向切面编程。

    找到AccessInterceptor.java这个类,实现HandlerInterceptor接口,实现preHandle方法

    1. @Override
    2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    3. if (handler instanceof HandlerInterceptor){
    4. //获取用户,并保存
    5. User user = getUser(request, response);
    6. UserContext.setUser(user);
    7. //获取限流注解
    8. HandlerMethod hm = (HandlerMethod) handler;
    9. AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
    10. if (accessLimit==null){
    11. return true;
    12. }
    13. //自定义接口限流:时间、访问数、是否需要登录
    14. //在conttoller方法上加上@AccessLimit(second=5,maxCount=5,needLogin=true)
    15. int seconds = accessLimit.seconds();
    16. int maxCount = accessLimit.maxCount();
    17. boolean needLogin = accessLimit.needLogin();
    18. String key = request.getRequestURI();
    19. if (needLogin){
    20. if (user==null){
    21. render(response, CodeMsg.SESSION_ERROR);
    22. return false;
    23. }
    24. key+="_"+user.getId();
    25. }else{
    26. //do nothing
    27. }
    28. //根据限流键值获取缓存
    29. AccessKey ak = AccessKey.withExpire();
    30. Integer count = redisService.get(ak, key, Integer.class);
    31. if (count==null){
    32. redisService.set(ak,key,1,seconds);
    33. } else if (count
    34. redisService.incr(ak,key);
    35. }else{
    36. render(response,CodeMsg.ACCESS_LIMIT_REACHED);
    37. return false;
    38. }
    39. }
    40. return true;
    41. }

    Spring中AOP的概念将在上面的代码中体现的淋漓尽致,首先是if判断是否继承自HandlerInterceptor,然后从request中获取token,进而得到当前的用户,得到用户后与ThreadLocal进行绑定,Threadlocal的底层是一个Map的结构。

    随后我们基于当前的handler处理器得到方法的注解,通过这个对象的获取,我们可以拿到注解中各个参数的值。

    如果说注解标记的方法需要登录后才能使用,恰巧获取的当前用户为空,需要返回给界面一些提示信息 ,比如像下面代码这样写

    1. /**
    2. * 把提示返回给客户端
    3. * @param response
    4. * @param cm
    5. * @throws Exception
    6. */
    7. private void render(HttpServletResponse response, CodeMsg cm) throws Exception{
    8. response.setContentType("application/json;charset=UTF-8");
    9. OutputStream out = response.getOutputStream();
    10. String str = JSON.toJSONString(Result.error(cm));
    11. out.write(str.getBytes("UTF-8"));
    12. out.flush();
    13. out.close();
    14. }

    之后我们需要根据限流键值对从redis中获取此时这个方法目前已被访问的次数。

    值得一提的是redisService.incr以及redisService.decr都是原子方法。

    至此切面写完了,我们需要把切面注入到Spring中

    来到WebConfig.java,实现WebMvcConfigurer接口,需要重写addArgumentResolvers还有addInterceptors。

    addInterceptors这个方法是将自定义的accessInterceptor注册进来就可以

    1. @Override
    2. public void addInterceptors(InterceptorRegistry registry) {
    3. registry.addInterceptor(accessInterceptor);
    4. }

    addArgumentReslvers这个是对Controller层传入的参数进行处理,将处理好的参数传给Controller里面的方法。详情请查看《WebMvcConfigurer中addArgumentResolvers方法的使用》

    我们在这里使用的是自己定义的UserArgumentResolver.java这个类。

    1. @Service
    2. public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    3. @Override
    4. public boolean supportsParameter(MethodParameter parameter) {
    5. Class clazz = parameter.getParameterType();
    6. return clazz== User.class;
    7. }
    8. @Override
    9. public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    10. return UserContext.getUser();
    11. }
    12. }

    我们来简单介绍一下这个实现了HandlerMethodArgumentResolver接口的自定义方法,只有参数是User.class时才会生效,生效后会返回一个User对象。要想深入理解可参考:

    https://blog.csdn.net/ocean35/article/details/105892788
    

    结束----

    自定义全局异常

    自定义异常类在项目中会经常遇到,主要帮助用户抛出自定义的异常,方便用户理解。

    1. public class GlobalException extends RuntimeException{
    2. private static final long serialVersionUID=1L;
    3. private CodeMsg cm;
    4. public GlobalException(CodeMsg cm){
    5. super(cm.toString());
    6. this.cm=cm;
    7. }
    8. public CodeMsg getCm(){
    9. return cm;
    10. }
    11. }

    因为继承自RuntimeException所以必须有个super方法。

    Redis配置类

    这里使用了Jedis的直接读且application.properties文件的方法,比较新颖,如果以后在项目中碰到业务场景需加入redis并且要求配置简便的情况下,可以考虑这种

    首先是RedisConfig这个配置 类,主要是与application.properties中的配置字段对应上。

    1. @Data
    2. @Component
    3. @ConfigurationProperties(prefix = "redis")
    4. public class RedisConfig {
    5. private String host;
    6. private int port;
    7. private int timeout;
    8. private String password;
    9. private int poolMaxTotal;
    10. private int poolMaxIdle;
    11. private int poolMaxWait;
    12. }

    @ConfigurationProperties(prefix="redis")这与application.properties相对应。

    接下来是jedisPool创建操作

    1. @Service
    2. public class RedisPoolFactory {
    3. @Autowired
    4. private RedisConfig redisConfig;
    5. @Bean
    6. public JedisPool JedisPoolFactory(){
    7. JedisPoolConfig poolConfig = new JedisPoolConfig();
    8. poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
    9. poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
    10. poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait()*1000);
    11. JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
    12. redisConfig.getTimeout() * 1000, redisConfig.getPassword(), 0);
    13. return jp;
    14. }
    15. }

    核心的一点就是创建JedisPool操作。

    redis分布式锁解析

    知识点

    setIfAbsent(key,value,时长,时长单位):设置之前先判断key值是否存在

    setIfAbsent  是redis(setnx)在java中的用法

    思路

    1.根据秒杀的业务场景,我们需要对秒杀商品的库存数生成一个锁,更改库存数的时候先判断库存锁是否有效和存在

    2.如果库存锁存在返回一个错误提示

    3.如果库存锁不存在,对库存数量进行操作

    4.执行完整个逻辑后删除库存锁

    锁的设计

    stringRedisTemplate.opsForValue().setIfAbsent(realKey,str,exTime, TimeUnit.SECONDS);

    realKey作为key

    str作为value

    exTime代表过期时间

    TimeUnit.SECONDs代表时间单位

    WebMvcConfigurer中addArgumentResolvers方法的使用

            在Springboot中的WebMvcConfigurer接口在Web开发中经常被使用,例如配置拦截器、配置ViewController、配置Cors跨域等

    本文主要讲解另一个方法:addArgumentResolvers()在实例中的应用。

    一、方法作用 该方法可以用在对于Controller中方法参数传入之前对该参数进行处理。然后将处理好的参数在传给Controller中的方法。 官方API文档解释:添加解析器以支持自定义控制器方法参数类型。 这不会覆盖对解析处理程序方法参数的内置支持。要自定义对参数解析的内置支持,请RequestMappingHandlerAdapter直接配置。

    二、场景描述 在权限场景中,通常会有要求用户登录之后才能访问的场景。对于这些问题可以多种解决方案,如:使用Cookie+Session的会话控制、使用拦截器、使用SpringSecurity或shiro等权限管理框架等。 这里使用Cookie+Session处理。处理的逻辑为: 用户第一次登录之后会得到一个cookie,在以后每次的访问过程中都会携带Cookie进行访问。在后台的Controller中对于需要登录权限的访问接口都要先获取Cookie中的Token,再使用Token从session中获取用户登录信息来判断用户登录情况决定是否放行。

  • 相关阅读:
    环保行业B2B撮合管理系统实现产业优化升级,提升企业业务能力
    社群运营怎么做?
    tkinter和Tkinter的区别
    详细讲解什么是工厂模式
    web3之链上情报平台Arkham
    【VSCode实战】转换大小写快捷键
    10年经验测试经理跳槽,5面成功拿下大厂 P7 Offer,真是麻雀啄了牛屁股,雀氏牛皮呀
    闲谈:3AC到底发生了什么?
    产品流程图设计
    嵌入式分享合集94
  • 原文地址:https://blog.csdn.net/qq_28606665/article/details/133898080