秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter ) -02

秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter ) -02

自定义参数解析器获取 User 对象

实现 WebMvcConfigurer ,优化登录-扩展知识点

获取浏览器传递的 cookie 值,进行参数解析,直接转成 User 对象,继续传递.

  1. 配置相关转换为对象的,配置类。UserArgumentResolver

package com.rainbowsea.seckill.config;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * @author Rainbowsea
 * @version 1.0
 * UserArgumentResolver: 自定义的一个解析器
 */
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    //装配UserService
    @Resource
    private UserService userService;

    //判断你当前要解析的参数类型是不是你需要的?
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //获取参数是不是user类型
        Class<?> aClass = parameter.getParameterType();
        //如果为t, 就执行resolveArgument
        return aClass == User.class;
    }

    //如果上面supportsParameter,返回T,就执行下面的resolveArgument方法
    //到底怎么解析,是由程序员根据业务来编写
    @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.hasText(ticket)) {
            return null;
        }
        //不急,一会老师会给小伙伴debug
        从Redis来获取用户
        User user = userService.getUserByCookieByRedis(ticket, request, response);

        return user;
        //return UserContext.getUser();
    }
}

自定义解析器,如果不是 User 对象的,是其他对象类的处理扩展。

  1. 同样还需要编写一个 WebConfig的配置类。

package com.rainbowsea.seckill.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author RainbowSea
 * @version 1.0
 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    //装配
    @Resource
    private UserArgumentResolver userArgumentResolver;


    //静态资源加载
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

    //这里加入我们自定义的解析器到 HandlerMethodArgumentResolver列表中
    //这样自定义的解析器工作
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

配置好,自定义的参数解析器获取 User 对象,就可以修改

package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


/**
 * 商品列表处理
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {


    @Resource
    private UserService userService;


    

    // 跳转到商品列表页
    @RequestMapping(value = "/toList")
    public String toList(Model model,User user) {
        // //验证部分的代码,可以注销了,WebMvcConfigurer使用 mvc进行优化,避免每次都要

        if (null == user) { // 用户没有成功登录
            return "login";
        }


        // 将 user 放入到 model,携带该下一个模板使用
        model.addAttribute("user", user);

        return "goodsList";
    }
}


// 跳转到商品列表页
    //@RequestMapping(value = "/toList")
    //public String toList(HttpSession session,
    //                     Model model,
    //                     @CookieValue("userTicket") String ticket,
    //                     ) {
    /*@RequestMapping(value = "/toList")
    public String toList(Model model,
                         @CookieValue("userTicket") String ticket,
                         HttpServletRequest request,
                         HttpServletResponse response
    ) {
        //  @CookieValue("userTicket") String ticket 注解可以直接获取到,对应 "userTicket" 名称
        // 的cookievalue 信息
        if (!StringUtils.hasText(ticket)) {
            return "login";
        }


        // 通过 cookieVale 当中的 ticket 获取 session 中存放的 user
        //User user = (User) session.getAttribute(ticket);

        // 改为从 Redis 当中获取
        User user = userService.getUserByCookieByRedis(ticket, request, response);

        if (null == user) { // 用户没有成功登录
            return "login";
        }


        // 将 user 放入到 model,携带该下一个模板使用
        model.addAttribute("user", user);

        return "goodsList";
    }*/

完成测试,启动项目,用户登录

浏览器输入 http://localhost:8080/login/toLogin,如果登录成功过,可进入商品列表

浏览器输入: http://localhost:8080/login/toLogin, 如果登录成功过,可进入商品列表

查看 Redis, 会看到登录用户信息

秒杀商品列表

秒杀商品的主要代码:没有进行秒杀优化的 V 1.0 版本

package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.rainbowsea.seckill.mapper.OrderMapper;
import com.rainbowsea.seckill.pojo.Order;
import com.rainbowsea.seckill.pojo.SeckillGoods;
import com.rainbowsea.seckill.pojo.SeckillOrder;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.OrderService;
import com.rainbowsea.seckill.service.SeckillGoodsService;
import com.rainbowsea.seckill.service.SeckillOrderService;
import com.rainbowsea.seckill.vo.GoodsVo;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @author huo
 * @description 针对表【t_order】的数据库操作Service实现
 * @createDate 2025-04-26 20:48:19
 */
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
        implements OrderService {

    @Resource
    private SeckillGoodsService seckillGoodsService;

    @Resource
    private OrderMapper orderMapper;


    @Resource
    private SeckillOrderService seckillOrderService;


    /**
     * 秒杀商品,减少库存
     *
     * @param user
     * @param goodsVo
     * @return Order
     */
    @Override
    public Order seckill(User user, GoodsVo goodsVo) {

        // 查询后端的库存量进行减一
        SeckillGoods seckillGoods = seckillGoodsService
                .getOne(new QueryWrapper<SeckillGoods>()
                        .eq("goods_id", goodsVo.getId()));

        // 完成一个基本的秒杀操作【这快不具原子性】,后面在高并发的情况下,还会优化
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
        seckillGoodsService.updateById(seckillGoods);

        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goodsVo.getId());
        order.setDeliveryAddrId(0L); // 这里随便设置了一个初始值
        order.setGoodsName(goodsVo.getGoodsName());
        order.setGoodsCount(1);
        order.setGoodsPrice(seckillGoods.getSeckillPrice());

        // 保存 order 账单信息
        orderMapper.insert(order);

        // 生成秒杀商品订单~
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setGoodsId(goodsVo.getId());
        // 这里秒杀商品订单对应的 order_id 是从上面添加 order 后获取到的
        seckillOrder.setOrderId(order.getId());
        seckillOrder.setUserId(user.getId());

        // 保存 seckillOrder
        seckillOrderService.save(seckillOrder);

        return order;
    }
}





秒杀 Controller 页面处理:

package com.rainbowsea.seckill.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rainbowsea.seckill.pojo.Order;
import com.rainbowsea.seckill.pojo.SeckillOrder;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.GoodsService;
import com.rainbowsea.seckill.service.OrderService;
import com.rainbowsea.seckill.service.SeckillOrderService;
import com.rainbowsea.seckill.vo.GoodsVo;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;

@Controller
@RequestMapping("/seckill")
public class SeckillController {


    // 装配需要的组件/对象
    @Resource
    private GoodsService goodsService;

    @Resource
    private SeckillOrderService seckillOrderService;


    @Resource
    private OrderService orderService;


    /**
     * 方法: 处理用户抢购请求/秒杀
     * 说明: 我们先完成一个 V1.0版本,后面在高并发的情况下,还会继续优化
     *
     * @param model   返回给模块的 model 信息
     * @param user    User 通过用户使用了,自定义参数解析器获取 User 对象,
     * @param goodsId 秒杀商品的 ID 信息
     * @return 返回到映射在 resources 下的 templates 下的页面
     */
    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("秒杀 V 1.0 ");

        if (null == user) { //用户没有登录
            return "login";
        }

        // 登录了,则返回用户信息给下一个模板内容
        model.addAttribute("user", user);

        // 获取到 GoodsVo
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);

        // 判断库存
        if (goodsVo.getStockCount() < 1) {  // 没有库存,不可以购买
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail"; // 返回一个错误页面
        }

        // 判断用户是否复购-判断当前购买用户的ID和购买商品id是否已经在商品秒杀表当中存在了。
        SeckillOrder seckillOrder = seckillOrderService.getOne(
                new QueryWrapper<SeckillOrder>()
                        .eq("user_id", user.getId())
                        .eq("goods_id", goodsId)
        );  // 这里的 column 不可以随便写,要对应上数据表当中的对应的“字段名”

        if (seckillOrder != null) {  // 不为 null ,说明用户购买过了,进行了复购
            model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage());
            return "secKillFail"; // 返回一个错误页面
        }

        // 抢购
        Order order = orderService.seckill(user, goodsVo);
        if (order == null) { // 说明抢购失败了,由于什么原因
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail"; // 返回一个错误页面
        }

        // 走到这里,说明抢购成功了,将信息,通过 model 返回给页面
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);

        System.out.println("秒杀 V 1.0 ");

        return "orderDetail";  // 进入到订单详情页


    }
}

补充:

fulshdb # 清空 Redis 当中的 keys 的所有内容

Jmeter 工具

由于涉及的篇幅过多,关于 Jmeter 的具体使用,大家可以移步至:✏️✏️✏️ https://blog.csdn.net/qq_58981141/article/details/146150407。进行学习。

使用 Redis 缓存一些查询变化少的页面

首先,将一些查询变化少的页面,添加加载到 Redis 当中。这样我们查询加载某一个页面的时候,只需要查询后端 DB 数据库一次,然后将其缓存大 Redis 当中。 我们就可以直接从 Redis 缓存数据当中获取到,如果从而就不需要对后端数据库 DB 进行查询了,减少了对 DB 数据库查询,而是从 Redis 当中查找。这里我们设置将 Redis 缓存时间设置为 60 秒,当过期时,我们就重新从后端 DB 数据库当中查询获得,然后,再次缓存到 Redis 当中。需要注意的是:我们缓存的页面,一般必须是为一种查询变化少的页面,数据变化少的页面。



  1. 多用户在查看商品列表和商品详情的时候,每一个用户都需要到 DB 查询。
  2. 对 DB 查询的压力很大,比如 10000 人,在 1 分钟都查看商品列表,就会有 10000 次对 DB 操作。
  3. 但是我们商品信息并不会频繁的变化,所以你查询回来的结果都是一样的。
  4. 我们可以通过 Redis 缓存页面来进行优化,这样可以将 1 分钟内多次查询 DB,优化成 1 次查询,减少 DB 压力。

商品页面-使用 Redis 缓存

package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.GoodsService;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.vo.GoodsVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.concurrent.TimeUnit;


/**
 * 商品列表处理
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {


    @Resource
    private UserService userService;

    @Resource
    private GoodsService goodsService;


    // Redis 渲染
    @Resource
    private RedisTemplate redisTemplate;


    // Thymeleaf 手动渲染
    @Resource
    private ThymeleafViewResolver thymeleafViewResolver;


    /**
     * 跳转到商品列表页 ,使用上 Redis 缓存的页面
     *
     * @param model
     * @param user     用户
     * @param request
     * @param response
     * @return html 返回的是一个页面对象
     */
    @RequestMapping(value = "/toList", produces = "text/html;charset=utf-8")
    @ResponseBody  // 使用了 Redis 缓存页面需要添加
    public String toList(Model model, User user,
                         HttpServletRequest request,
                         HttpServletResponse response) {
        // //验证部分的代码,可以注销了,WebMvcConfigurer使用 mvc进行优化,避免每次都要


        // 先从 Redis 中获取页面,如果不为空,直接返回页面
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String html = (String) valueOperations.get("goodsList");
        // html 不为空,说明从 Redis 当中获取到了内容,不需要从DB当中获取
        if (StringUtils.hasText(html)) {
            return html;
        }

        if (null == user) { // 用户没有成功登录
            return "login";
        }


        // 将 user 放入到 model,携带该下一个模板使用
        model.addAttribute("user", user);

        //展示商品
        model.addAttribute("goodsList", goodsService.findGoodsVo());

        // 如果为从 Redis 中取出页面为 null,则手动渲染,存入到 Redis 中
        WebContext webContext = new WebContext(request, response,
                request.getServletContext(),
                request.getLocale(), model.asMap());

        html = thymeleafViewResolver.getTemplateEngine().process("goodsList", webContext);

        if (StringUtils.hasText(html)) { // html 不为空,进入 if ,说明从页面当中获取的页面,存入到 Redis 当中

            // 表示: 每 60s 更新一次 Redis 页面缓存,即60s后,该页面缓存失效,Redis会清除页面缓存。
            valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);
            // 注意:我们这里的这个 goodsList 不可以随便写,而是同我们本身项目当中的 /templates 目录下存在的页面当中映射导入的
        }
        //return "goodsList";

        return html;
    }





}

完成测试

启动相关服务和项目

登录->进入商品列表页

查看 Redis

商品详情-页面 使用 Redis 缓存

package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.GoodsService;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.vo.GoodsVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.concurrent.TimeUnit;


/**
 * 商品列表处理
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {


    @Resource
    private UserService userService;

    @Resource
    private GoodsService goodsService;


    // Redis 渲染
    @Resource
    private RedisTemplate redisTemplate;


    // Thymeleaf 手动渲染
    @Resource
    private ThymeleafViewResolver thymeleafViewResolver;


    /**
     * 跳转商品详情页面,使用 上 Redis 缓存
     *
     * @param model
     * @param user
     * @param goodsId
     * @return String 跳转到对应 templates 下对应的html页面
     */
    @RequestMapping(value = "/toDetail/{goodsId}", produces = "text/html;charset=utf-8")
    @ResponseBody  // 注意:这里需要返回一个我们需要的 html 进行一个解析的渲染。所以这个注解必须要有
    public String toDetail(Model model, User user,
                           @PathVariable("goodsId") Long goodsId,
                           HttpServletResponse response,
                           HttpServletRequest request) {


        //使用页面缓存
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String html = (String) valueOperations.get("goodsDetail:" + goodsId);
        if (StringUtils.hasText(html)) {
            return html;
        }


        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);

        // 说明: 返回秒杀商品详情时,同时返回该商品的秒杀状态和秒杀的剩余时间
        // 为了配合前端展示秒杀前端的状态
        // 1. 变量 secKillStatus 秒杀状态 0表示:秒杀未开始,1:秒杀进行中,2:秒杀已经结束
        // 2. 变量 remainSeconds 剩余秒数: >0表示:还有多久开始秒杀, 0:表示秒杀进行中,-1:表示秒杀结束


        // 秒杀开始时间
        Date startDate = goodsVo.getStartDate();
        // 秒杀结束时间
        Date endDate = goodsVo.getEndDate();
        // 当前时间
        Date nowDate = new Date();

        int secKillStatus = 0;
        int remainSeconds = 0;

        // 如果nowDate 在 startDate 前,说明还没有开始秒杀
        if (nowDate.before(startDate)) {
            //  startDate.getTime() 返回的是毫秒 , 1000 表示 每秒
            secKillStatus = 0;  // 秒杀未开始
            remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) / 1000);
        } else if (nowDate.after(endDate)) {
            secKillStatus = 2; // 表示秒杀已经结束
            remainSeconds = -1; // 表示秒杀已经结束
        } else {
            // 秒杀进行中
            secKillStatus = 1;
            remainSeconds = 0;
        }


        // 将 secKillStatus 和 remainSeconds 放入到 model,携带给下模板页使用
        model.addAttribute("secKillStatus", secKillStatus);
        model.addAttribute("remainSeconds", remainSeconds);

        model.addAttribute("goods", goodsVo);


        // 如果为从 Redis 中取出页面为 null,则手动渲染,存入到 Redis 中
        WebContext webContext = new WebContext(request, response,
                request.getServletContext(),
                request.getLocale(), model.asMap());

        html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", webContext);

        if (StringUtils.hasText(html)) { // html 不为空,进入 if ,说明从页面当中获取的页面,存入到 Redis 当中

            // 表示: 每 60s 更新一次 Redis 页面缓存,即60s后,该页面缓存失效,Redis会清除页面缓存。
            valueOperations.set("goodsDetail", html, 60, TimeUnit.SECONDS);
            // 注意:我们这里的这个 goodsList 不可以随便写,而是同我们本身项目当中的 /templates 目录下存在的页面当中映射导入的
        }


        return html;
        //return "goodsDetail";

    }
}


完成测试

启动相关服务和项目

登录->进入商品列表页->点击商品详细

查看 Redis

注意观察, 商品列表页和商品详情页, 失效是否生效了

压测-商品列表接口

Jmeter 设置

准备好多用户测试脚本, 保证多用户可以登录, 如果环境变化了, 重新生成即可

配置 Jmeter

测试结果:


对象缓存问题

主要:登录时,我们首先读取的是 Redis 是否存在,但是当我们的用户更新了,就会导致 Redis 当中存储的信息是不对的,和 DB 当中的数据不一致。

当然有小伙伴会有这样的疑虑, 如果我们商品信息修改了, 会存在短时间内(比如 1 分

钟), Redis 页面缓存和商品信息不一致的问题, 怎么办?

2、思路 1 : 可以将 Redis 页面缓存更新时间减小, 比如设置成 10s, 甚至更短.

3、思路 2 : 可以在更新商品信息时, 同时更新 Redis 页面缓存, 通常完整的系统会有后

台管理程序, 更新 Redis 页面缓存的原理和机制是一样的


当用户登录成功后, 就会将用户对象缓存到 Redis

好处是解决了分布式架构下的 Session 共享问题


但是也带来新的问题, 如果用户信息改变, 存在 用户信息和 Redis 缓存用户对象数据

不一致问题-也就是对象缓存问题

解决思路:

  1. 编写方法, 当用户信息变化时, 就更新用户在 DB 的信息, 同时删除该用户在 Redis 的

缓存对象

  1. 这样用户就需要使用新密码重新登录, 从而更新用户在 Redis 对应的缓存对象.
  2. 我们以修改用户密码为例来演示, 用户其它信息修改类似
  3. 用户信息修改接口,通常是在后台系统调用的,老师写出即可-然后使用

UserService.java 增加 API 接口

package com.rainbowsea.seckill.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service
 * @createDate 2025-04-24 15:38:01
 */
public interface UserService extends IService<User> {


    /**
     * 更改密码 ,更新 Redis 当中的用户缓存信息。
     * @param userTicket
     * @param password
     * @param request
     * @param response
     * @return
     */
    RespBean updatePassword
            (String userTicket, String password,
             HttpServletRequest request, HttpServletResponse response);
}


UserServiceImpl.java , 实现 API 接口

package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.UUIDUtil;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service实现
 * @createDate 2025-04-24 15:38:01
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {


    @Resource
    private UserMapper userMapper;

    @Resource
    private RedisTemplate redisTemplate;



    /**
     * 更改用户密码,同时立马更新缓存到 Redis 当中的信息
     * @param userTicket
     * @param password
     * @param request
     * @param response
     * @return
     */
    @Override
    public RespBean updatePassword(String userTicket,
                                   String password,
                                   HttpServletRequest request,
                                   HttpServletResponse response) {

        // 更新用户密码,同时删除用户在 Redis的缓存对象
        User user = this.getUserByCookieByRedis(userTicket,request,response);

        if(user == null) {
            // 抛出异常
            throw new GlobalException(RespBeanEnum.MOBILE_NOT_EXIST);
        }

        // 设置新密码
        user.setPassword(MD5Util.inputPassToDBPass(password,user.getSlat()));

        int i = userMapper.updateById(user);
        if(i == 1) {  // 更新成功
            redisTemplate.delete("user:"+userTicket);
            return RespBean.success();
        }


        return RespBean.error(RespBeanEnum.PASSWROD_UPDATE_FAIL);
    }


}






UserController.java, 增加 API

package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.vo.RespBean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * 用户登录,返回用户信息
 * <p>
 * 返回用户信息,同时我们也演示如何携带参数
 */
@Controller
@RequestMapping("/user")
public class UserController {


    @Resource
    private UserService userService;

    /**
     * 返回用户信息,同时我们也演示如何携带参数
     *
     * @param user    用户信息,这里的 User对象,我们是通过使用我们上面配置的一个 UserArgumentResolver 自定义自定义参数解析器获取 User 对象
     * @param address 地址
     * @return RespBean
     */
    @RequestMapping("/info")
    @ResponseBody
    public RespBean info(User user, String address) {
        return RespBean.success(user);
    }

    @RequestMapping("/updpwd")
    @ResponseBody
    public RespBean updatePassword(String userTicket,
                                   String password,
                                   HttpServletRequest request,
                                   HttpServletResponse response) {
        return userService.updatePassword(userTicket, password, request, response);

    }
}

在 Redis 找一个测试用户

在重新使用 13300000257 登录, 会重新生成一个 Redis 用户缓存对象

特别提醒:测试完毕后, 将我们的数据库用户和 config.txt 多用户脚本恢复原来的状态

提示:再进行操作前,记得把 Redis 执行 flushdb , 给一个干净的环境.

注意点:这里我们采用的是虚拟机,而我们安装的 Redis 是安装在虚拟机当中的,可能存在 IP 地址的变化,对应我们项目当中的 Redis 的 IP 地址,需要作出相应的修改。

通过 MySQL 默认锁,解决复购,超购问题

当多用户在高并发秒杀商品时, 不能出现超卖和生成多条订单

修改:OrderServiceImpl.java,执行更新 MySQL 数据库的操作,该方法上添加上,@Transactional 注解 ,表示启用 MySQL 默认的锁,进行事务控制处理。

注意:这里我们存储在 Redis 当中的 key-value 的设计是:key表示:order:userId:goodsId Value表示订单 seckillOrder

package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.rainbowsea.seckill.mapper.OrderMapper;
import com.rainbowsea.seckill.pojo.Order;
import com.rainbowsea.seckill.pojo.SeckillGoods;
import com.rainbowsea.seckill.pojo.SeckillOrder;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.OrderService;
import com.rainbowsea.seckill.service.SeckillGoodsService;
import com.rainbowsea.seckill.service.SeckillOrderService;
import com.rainbowsea.seckill.vo.GoodsVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * @author huo
 * @description 针对表【t_order】的数据库操作Service实现
 * @createDate 2025-04-26 20:48:19
 */
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
        implements OrderService {

    @Resource
    private SeckillGoodsService seckillGoodsService;

    @Resource
    private OrderMapper orderMapper;


    @Resource
    private SeckillOrderService seckillOrderService;


    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 秒杀商品,减少库存,V2.0 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 ,
     * 添加上 @Transactional,注解进行一个MySQL 默认的事务隔离级别
     *
     * @param user
     * @param goodsVo
     * @return Order
     */
    @Transactional
    @Override
    public Order seckill(User user, GoodsVo goodsVo) {

        // 查询后端的库存量进行减一
        SeckillGoods seckillGoods = seckillGoodsService
                .getOne(new QueryWrapper<SeckillGoods>()
                        .eq("goods_id", goodsVo.getId()));

        //分析
        // 1. MySQL 在默认的事务隔离级别 【REPEATABLE-READ】 下
        // 2. 执行 update 语句时,会在事务中锁定要更新的行
        // 3. 这样可以防止其它会话在同一行执行 update,delete

        // 说明: 只要在更新成功时,返回 true,否则返回 false
        // column 必须是,数据库表当中的字段,不可以随便写
        boolean update = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
                .setSql("stock_count = stock_count - 1")
                .eq("goods_id", goodsVo.getId())
                .gt("stock_count", 0));  // gt  表示大于

        if(!update) {  // 如果更新失败,说明已经没有库存了
            return null;
        }

        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goodsVo.getId());
        order.setDeliveryAddrId(0L); // 这里随便设置了一个初始值
        order.setGoodsName(goodsVo.getGoodsName());
        order.setGoodsCount(1);
        order.setGoodsPrice(seckillGoods.getSeckillPrice());

        // 保存 order 账单信息
        orderMapper.insert(order);

        // 生成秒杀商品订单~
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setGoodsId(goodsVo.getId());
        // 这里秒杀商品订单对应的 order_id 是从上面添加 order 后获取到的
        seckillOrder.setOrderId(order.getId());
        seckillOrder.setUserId(user.getId());

        // 保存 seckillOrder
        seckillOrderService.save(seckillOrder);
        // 将生成的秒杀订单,存入到 Redis,这样在查询某个用户是否已经秒杀了这个商品时
        // 直接到 Redis 中查询,起到优化效果
        // key表示:order:userId:goodsId  Value表示订单 seckillOrder
        redisTemplate.opsForValue().set("order:"+user.getId()+":" +
                goodsVo.getId(),
                seckillOrder);

        return order;
    }
}





修改:SeckillController.java 使用上,我们添加了,事务控制的接口,进行调用。

package com.rainbowsea.seckill.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rainbowsea.seckill.pojo.Order;
import com.rainbowsea.seckill.pojo.SeckillOrder;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.GoodsService;
import com.rainbowsea.seckill.service.OrderService;
import com.rainbowsea.seckill.service.SeckillOrderService;
import com.rainbowsea.seckill.vo.GoodsVo;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;

@Controller
@RequestMapping("/seckill")
public class SeckillController {


    // 装配需要的组件/对象
    @Resource
    private GoodsService goodsService;

    @Resource
    private SeckillOrderService seckillOrderService;


    @Resource
    private OrderService orderService;


    @Resource
    private RedisTemplate redisTemplate;


    /**
     * 方法: 处理用户抢购请求/秒杀
     * 说明: 我们先完成一个 V2.0版本,利用 MySQL默认的事务隔离级别【REPEATABLE-READ】
     *
     * @param model   返回给模块的 model 信息
     * @param user    User 通过用户使用了,自定义参数解析器获取 User 对象,
     * @param goodsId 秒杀商品的 ID 信息
     * @return 返回到映射在 resources 下的 templates 下的页面
     */
    @RequestMapping(value = "/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        System.out.println("秒杀 V 1.0 ");

        if (null == user) { //用户没有登录
            return "login";
        }

        // 登录了,则返回用户信息给下一个模板内容
        model.addAttribute("user", user);

        // 获取到 GoodsVo
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);

        // 判断库存
        if (goodsVo.getStockCount() < 1) {  // 没有库存,不可以购买
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail"; // 返回一个错误页面
        }

        // 判断用户是否复购-直接到 Redis 当中获取(因为我们抢购成功直接
        // 将表单信息存储到了Redis 当中了。 key表示:order:userId:goodsId  Value表示订单 seckillOrder),
        // 获取对应的秒杀订单,如果有,则说明该
        // 用户已经桥抢购了,每人限购一个
        SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" +
                goodsVo.getId()); // 因为我们在 Redis 当中的 value值就是 SeckillOrder 订单对象,所以这里可以直接强制类型转换
        if(null != o) { // 不为null,说明 Redis 存在该用户订单信息,说明该用户已经抢购了该商品
            model.addAttribute("errmsg",RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中
            return "secKillFail"; // 返回一个错误页面
        }

        // 抢购
        Order order = orderService.seckill(user, goodsVo);
        if (order == null) { // 说明抢购失败了,由于什么原因
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail"; // 返回一个错误页面
        }

        // 走到这里,说明抢购成功了,将信息,通过 model 返回给页面
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);

        System.out.println("秒杀 V 2.0 ");

        return "orderDetail";  // 进入到订单详情页


    }
}

测试:

  • 确保多用户的 userticket 已经正确的保存到 Redis 中
  • 启动线程组,进行测试
  • 测试结果, 不在出现超卖问题

最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值