商城项目秒杀功能后端

文章介绍了商城项目中秒杀功能的整体架构和流程,包括商品设置、活动页面、订单处理、秒杀限制和结果显示。具体实现涉及定时任务导入秒杀商品、使用Redis存储和处理库存、利用RabbitMQ进行消息队列通信以及状态位的共享。此外,还讨论了并发控制和防止恶意行为的措施,确保秒杀活动的公平性和系统稳定性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概要

商城项目中的秒杀功能是指在特定时间段内,以较低的价格或特殊的优惠条件销售一定数量的商品。这个功能通常被用于吸引用户、提升销量和增加用户粘性。

整体架构流程

主要使用spring boot+spring cloud+redis+RabbitMQ。

商品中的秒杀功能一般包括以下几个方面
1.秒杀商品设置,商城项目一般可以在特定时间选择一些商品作为秒杀商品,并设置秒杀的特定时间、结束时间和秒杀数量等参数
2.秒杀活动页面。商城项目会在秒杀活动开始前提前编写秒杀活动页面,包括秒杀活动列表,秒杀商品详情,包括商品的图片、名称、原价、秒杀价等,并显示秒杀的倒计时。
3.秒杀订单处理,用户在秒杀期间,商城项目需要判断用户购买资格,判断库存是否充足,生成订单、减少库存等
4.秒杀限制,商城项目一般会对秒杀功能进行一些限制,例如每个用户只能参与一次秒杀活动等。
5.秒杀结果显示,秒杀成功后,会显示秒杀的结果。

具体实现

1.秒杀活动当天导入秒杀商品

 //利用定时任务导入秒杀商品 每天凌晨1点导入商品,这时用户量比较小
    @Scheduled(cron = "0 0 1 * * ?")
    public void task1(){
        System.out.println("触发的时间为"+new Date());
 //给秒杀模块发送消息导入商品
        rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK,MqConst.ROUTING_TASK_1,"");
    }
//根据定时任务导入商品
@RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),
            value = @Queue(value = MqConst.QUEUE_TASK_1),
            key = {MqConst.ROUTING_TASK_1}
    ))
    @SneakyThrows
    public void importTpRedis(Message message, Channel channel){
        QueryWrapper<SeckillGoods> queryWrapper=new QueryWrapper<>();
        //查询符合条件的秒杀商品集合 是当天时间的、库存大于0的、状态为等于1的
        queryWrapper.eq("DATE_FORMAT(start_time,'%Y-%m-%d')", DateUtil.formatDate(new Date()))
                .gt("stock_count",0).eq("status",1);
        List<SeckillGoods> seckillGoodsList = seckillGoodsService.list(queryWrapper);
        //遍历符合条件的商品
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            //如果缓存中已经有了这个商品就直接跳过此次循环
            if (redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).hasKey(seckillGoods.getSkuId().toString())){
                continue;
            }
            //如果没有,将其放入缓存
            redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(),seckillGoods);
            //将库存放入集合队列中实现原子性防止超卖  根据库存数量
            for (Integer i = 0; i < seckillGoods.getStockCount(); i++) {
                redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX+seckillGoods.getSkuId().toString()).leftPush(seckillGoods.getSkuId().toString());
            }
            //更新状态位通过Redis发布订阅
            redisTemplate.convertAndSend("seckillpush",seckillGoods.getSkuId()+":1");

        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }

需要注意的是redis的发布订阅模式:
用这个方式是由于,由于在内存中取数据要比在redis中取数据更快,所以可以把商品的状态位放入内存中,但是由于我们的项目是分布式的、每一个微服务可能都会有多个服务器,这时由于每个服务器都有不同的内存,就会导致状态位无法共享,因此我们可以采用Redis的发布订阅模式,对同一个应用的多个节点进行共享。

@Configuration
public class RedisChannelConfig {
    /**
     * 配置监听器
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(MessageListenerAdapter messageListenerAdapter, RedisConnectionFactory redisConnectionFactory){
        RedisMessageListenerContainer messageListenerContainer=new RedisMessageListenerContainer();
        //订阅主题
        messageListenerContainer.addMessageListener(messageListenerAdapter,new PatternTopic("seckillpush"));
        messageListenerContainer.setConnectionFactory(redisConnectionFactory);
        //这个container 可以添加多个 messageListener
        return messageListenerContainer;
    }


    /**
     * 配置适配器
     */
    @Bean
    public MessageListenerAdapter messageListenerAdapter(MessageReceive messageReceive){
        //这个地方 是给 messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”
        //也有好几个重载方法,这边默认调用处理器的方法 叫handleMessage 可以自己到源码里面看
        return new MessageListenerAdapter(messageReceive,"receiveMessage");

    }

    /**
     * 配置redisTemplate
     */
    @Bean
    StringRedisTemplate template(RedisConnectionFactory redisConnectionFactory){
        //因为StringRedisTemplate里面有redisConnectionFactory
        return new StringRedisTemplate(redisConnectionFactory);
    }


}
@Component
public class MessageReceive {
    /**
     * 接收消息的方法
     */
    public void receiveMessage(String message){
       message= message.replaceAll("\"","");
        String[] split = message.split(":");
        if (split!=null && split.length==2){
            CacheHelper.put(split[0],split[1]);
        }
    }
}
//状态位信息都放入这里面
public class CacheHelper {
    //ConcurrentHashMap是线程安全的
    public static final Map<String,Object> cacheMap=new ConcurrentHashMap<>();

    /**
     * 放入缓存
     * @param key
     * @param value
     */
    public static void put(String key,Object value){
        cacheMap.put(key,value);
    }

    /**
     * 得到缓存
     * @param key
     * @return
     */
    public static Object get(String key){
        return cacheMap.get(key);
    }
    /**
     * 清除缓存
     */
    public static  void removeAll(){
        cacheMap.clear();
    }

    public static void remove(String key){
        cacheMap.remove(key);
    }


}

2.完成秒杀商品列表和详情

	//秒杀商品列表
   @GetMapping("/findAll")
    public List<SeckillGoods> seckillGoodsList() {
        return redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).values();
    }
 //详情
 @GetMapping("/getSeckillGoods/{skuId}")
    public SeckillGoods getSeckillGoods(@PathVariable Long skuId) {
        return (SeckillGoods) redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(skuId.toString());
    }

//themeleaf完成请求转发功能
@GetMapping("/seckill.html")
    public String  seckill(Model model){
    //通过openfeign接口调用
        List<SeckillGoods> seckillGoodsList = activityFeignClient.seckillGoodsList();
        model.addAttribute("list",seckillGoodsList);
        return "seckill/index";
    }
    @GetMapping("/seckill/{skuId}.html")
    public String detailSeckill(@PathVariable Long skuId,Model model){
        //通过openfeign接口调用
        SeckillGoods seckillGoods = activityFeignClient.getSeckillGoods(skuId);
        List<BaseAttrInfo> skuAttrList = productFeignClient.getAttrList(skuId);
        model.addAttribute("skuAttrList",skuAttrList);
        model.addAttribute("item",seckillGoods);
        return "seckill/item";

    }

秒杀列表展示:
在这里插入图片描述
点击商品进入商品详情
在这里插入图片描述
3.获取抢单码、秒杀抢单和检查抢单状态

 @GetMapping("/auth/getSeckillSkuIdStr/{skuId}")
    public Result getSeckillSkuIdStr(@PathVariable Long skuId, HttpServletRequest request) {
        //获取下单的用户id
        String userId = AuthContextHolder.getUserId(request);
        //获取商品详情
        SeckillGoods seckillGoods = this.getSeckillGoods(skuId);
        //判断商品时间是否允许
        Date curDate = new Date();
        if (DateUtil.dateCompare(seckillGoods.getStartTime(), curDate) && DateUtil.dateCompare(curDate, seckillGoods.getEndTime())) {
            //如果允许,就返回
            String seckillStr = MD5.encrypt(userId);
            return Result.ok(seckillStr);
        }
        return Result.fail().message("商品抢购时间已过期或还未开始");

获取抢单码是为了防止用户恶意访问页面,在后续下单的时候会进行判断抢单码是否相等,防止用户通过输入网址进入下单页面

获取抢单码之后,跳转到新的页面,将抢单码作为参数传出去,接着就可以进入秒杀抢单接口,在这里会进行
判断抢单码是否正确,判断状态位是否允许抢单,如果都可以的话,就可以发送消息到MQ队列,排着队等待后续处理

 @PostMapping("/auth/seckillOrder/{skuId}")
    public Result seckillOrder(@PathVariable Long skuId, HttpServletRequest request) {
        //获取传过来的下单码
        String skuIdStr = request.getParameter("skuIdStr");
        //获取用户id
        String userId = AuthContextHolder.getUserId(request);
        //验证下单码是否正确
        if (!MD5.encrypt(userId).equals(skuIdStr)) {
            return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);
        }
        //验证状态位是否正确
        String status = (String) CacheHelper.get(skuId.toString());
        //如果为空,表示不合法
        if (StringUtils.isEmpty(status)) {
            return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);
        }
        //如果为0说明已售罄
        if (status.equals("0")) {
            return Result.build(null, ResultCodeEnum.SECKILL_FINISH);
        }
        //如果为1说明还有商品
        if (status.equals("1")) {
            UserRecode userRecode = new UserRecode();
            userRecode.setUserId(userId);
            userRecode.setSkuId(skuId);

            rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);
        }
        return Result.ok();
    }

在消费者收到秒杀消息后,还需要再次判断状态位,防止商品已经售罄,如果售罄,不再往下执行,之后需要判断用户是否重复下单,可以利用Redis中的setnx进行判断,如果用户已经下过单,那么就表示setnx已经设置过,由于setnx只能设置一次,再次设置就会返回false,这样就能控制用户重复下单,如果用户重复下单,就不再往下执行,之后再次判断商品是否售罄,如果售罄,更新状态位不再往下执行,如果上述都没有问题,那么就可以生成临时订单,更新库存。

  @SneakyThrows
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MqConst.QUEUE_SECKILL_USER),
            exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_SECKILL_USER),
            key = {MqConst.ROUTING_SECKILL_USER}
    ))
    public void seckill(Message message, Channel channel, UserRecode userRecode){
        try {
            if (userRecode!=null){
                seckillGoodsService.seckillOrder(userRecode);
            }
        } catch (Exception e) {
            //出现问题记录日志
            e.printStackTrace();
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

    }

//service方法
   @Override
    public void seckillOrder(UserRecode userRecode) {
        String userId = userRecode.getUserId();
        Long skuId = userRecode.getSkuId();
        //判断状态位
        if (CacheHelper.get(skuId.toString()).equals("0")){
            return;
        }
        //判断用户是否重复下单
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(RedisConst.SECKILL_USER + userId, skuId.toString(),RedisConst.SECKILL__TIMEOUT, TimeUnit.SECONDS);
        if (!ifAbsent){
            return;
        }
        //取出商品
        String goodsId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX+skuId).rightPop();
        if (StringUtils.isEmpty(goodsId)){
            //如果为空说明商品售罄,更新状态位
            redisTemplate.convertAndSend("seckillpush",skuId+":0");
            return;
        }
        //生成临时订单,存入redis
        OrderRecode orderRecode=new OrderRecode();
        orderRecode.setOrderStr(MD5.encrypt(userId+skuId));
        orderRecode.setUserId(userId);
        orderRecode.setNum(1);
        orderRecode.setSeckillGoods(this.getSeckllGoods(skuId));
        redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).put(orderRecode.getUserId(),orderRecode);
        //更新库存
        this.updateStock(skuId);


    }

    private void updateStock(Long skuId) {
        ReentrantLock reentrantLock=new ReentrantLock();
        reentrantLock.lock();

        try {
            //获取库存剩余数
            Long size = redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX+skuId).size();

                //更新数据库
                SeckillGoods seckillGoods = this.getSeckllGoods(skuId);
                seckillGoods.setStockCount(size.intValue());
                seckillGoodsMapper.updateById(seckillGoods);
                //更新缓存
                redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(),seckillGoods);


        } finally {
            reentrantLock.unlock();
        }

    }

    private SeckillGoods getSeckllGoods(Long skuId) {
        QueryWrapper<SeckillGoods> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("sku_id",skuId);
        return this.getOne(queryWrapper);
    }

在这个页面秒杀下单之后会有一个方法定时请求后端接口检查下单状态,检查是否下单成功,是否抢单成功,是否商品已经售罄,是否正在排队,是否重复下单

 @GetMapping("/auth/checkOrder/{skuId}")
    public Result checkOrder(@PathVariable Long skuId, HttpServletRequest request) {
        String userId = AuthContextHolder.getUserId(request);
        //根据缓存判断用户是否存在得知是否在排队
        Boolean isExist = redisTemplate.hasKey(RedisConst.SECKILL_USER + userId);
        if (isExist) {

            //判断是否下单成功
            if (redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).hasKey(userId.toString())){
                //表示已经下单成功
                return Result.build(null,ResultCodeEnum.SECKILL_ORDER_SUCCESS);
            }
            //判断是否抢单成功
            if (redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).hasKey(userId.toString())) {
                //表示已经抢单成功
                return Result.build(null, ResultCodeEnum.SECKILL_SUCCESS);
            }
            //判断商品是否售罄
            String status = (String) CacheHelper.get(skuId.toString());
            if (!StringUtils.isEmpty(status)){
                if (status.equals("0")){
                    return Result.build(null,ResultCodeEnum.SECKILL_FINISH);
                }
            }else{
                //用户可能重复下单或者商品状态出现问题
                return Result.build(null,ResultCodeEnum.ILLEGAL_REQUEST);
            }
        }
        //如果不存在说明正在排队
        return Result.build(null, ResultCodeEnum.SECKILL_RUN);

    }

抢单成功页面:
在这里插入图片描述

4.抢单成功后生成订单
首先进入交易页面,通过theme leaf页面转发,获取用户地址数据,订单详情数据,总价格,总数量

  @GetMapping("/seckill/trade.html")
    public String trade(Model model){
    //通过openfeign远程调用
        Result<Map> trade = activityFeignClient.trade();
        if (trade.isOk()){
            model.addAllAttributes(trade.getData());
            return "/seckill/trade";
        }else{
            model.addAttribute("message",trade.getMessage());
            return "seckill/fail";
        }

    }
 @GetMapping("/auth/trade")
    public Result trade(HttpServletRequest request){
        Map<String,Object> map=new HashMap<>();
        String userId = AuthContextHolder.getUserId(request);
        //获取用户地址 通过openfeign远程调用
        
        List<UserAddress> userAddressList = userFeignClient.findUserAddressListByUserId(Long.parseLong(userId));
        //获取商品详情 
       OrderRecode orderRecode= (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId.toString());
        SeckillGoods seckillGoods = orderRecode.getSeckillGoods();
        OrderDetail orderDetail=new OrderDetail();
        orderDetail.setImgUrl(seckillGoods.getSkuDefaultImg());
        orderDetail.setSkuName(seckillGoods.getSkuName());
        orderDetail.setSkuNum(orderRecode.getNum());
        orderDetail.setOrderPrice(seckillGoods.getPrice());
        orderDetail.setSkuId(seckillGoods.getSkuId());
        List<OrderDetail> detailArrayList=new ArrayList<>();
        detailArrayList.add(orderDetail);
        //获取总价格和总数
        OrderInfo orderInfo=new OrderInfo();
        orderInfo.setOrderDetailList(detailArrayList);
        orderInfo.sumTotalAmount();
        BigDecimal totalAmount = orderInfo.getTotalAmount();
        Integer totalNum = orderRecode.getNum();
        map.put("totalAmount",totalAmount);
        map.put("totalNum",totalNum);
        map.put("detailArrayList",detailArrayList);
        map.put("userAddressList",userAddressList);
        return Result.ok(map);


    }

交易页面:
在这里插入图片描述
在这里插入图片描述

点击提交订单开始生成订单

 @PostMapping("/auth/submitOrder")
    public Result submitOrder(@RequestBody OrderInfo orderInfo,HttpServletRequest request){
        String userId = AuthContextHolder.getUserId(request);
        orderInfo.setUserId(Long.parseLong(userId));
        //利用open feign调用订单模块生成订单
        Long orderId = orderFeignClient.submitOrder(orderInfo);
        if(orderId==null){
            return Result.fail().message("下单失败");
        }
        //删除抢单成功之后生成的临时订单
        redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).delete(userId.toString());
        //保存下单成功的信息
        redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).put(userId.toString(),orderId.toString());
        return Result.ok(orderId);
    }

下单成功页面
在这里插入图片描述

小结

商城项目中的秒杀功能需要考虑并发访问量大、库存管理、订单处理、交易安全等方面的问题,因此在实现上需要注意性能优化、并发控制、数据一致性等方面的考虑。同时,商城项目还需要注意防止刷单、防止秒杀脚本等恶意行为,保障秒杀活动的公平性和用户体验。

<think>好的,用户想了解商城系统后端项目的结构设计,特别是模块划分的最佳实践。我需要先回想常见的MVC架构,以及现代微服务或分层架构的应用。可能需要分模块如用户、商品、订单、支付等。同时要考虑可扩展性和维护性。 首先,商城系统后端通常采用分层架构,比如控制器层、服务层、数据访问层。然后模块划分方面,需要按业务功能拆分成独立模块,比如用户中心、商品管理、订单处理、支付集成、库存管理、营销活动等。每个模块内部应该高内聚,低耦合。 还要考虑通用模块,比如权限认证、日志管理、配置中心、消息队列等基础设施。数据库设计方面,分库分表策略,读写分离,缓存机制如Redis的使用。微服务架构下可能需要API网关、服务注册发现、配置中心等组件。 最佳实践包括使用Spring Boot或Spring Cloud框架,模块化开发,领域驱动设计(DDD),RESTful API设计规范,接口版本控制,异常统一处理,参数校验,Swagger文档生成,以及持续集成部署流程。 需要确保回答结构清晰,分点说明,每个模块的功能和设计考虑,并给出技术选型的例子。可能还需要提到项目结构目录的组织方式,比如按功能分包还是按层次分包。同时注意高并发场景下的优化策略,比如缓存、异步处理、分布式事务等。 最后,生成相关问题时要围绕模块划分、技术选型、性能优化、安全措施等方面。比如如何设计高可用的用户认证模块,或者如何处理分布式事务的问题。</think>### 商城系统后端项目结构设计指南 #### 一、分层架构设计 1. **控制器层(Controller)** - 处理HTTP请求与响应 - 实现接口参数校验 - 调用服务层处理业务逻辑 - 示例路由设计: ```java @RestController @RequestMapping("/api/products") public class ProductController { @Autowired private ProductService productService; @GetMapping("/{id}") public ResponseEntity<ProductDTO> getProduct(@PathVariable Long id) { return ResponseEntity.ok(productService.getProduct(id)); } } ``` 2. **服务层(Service)** - 实现核心业务逻辑 - 处理事务管理 - 调用数据访问层和第三方服务 - 示例服务接口: $$Transaction\ annotation\ ensures\ atomic\ operations\ in\ order\ creation[^1]$$ ```java @Service @Transactional public class OrderServiceImpl implements OrderService { public Order createOrder(OrderCreateRequest request) { // 库存校验、价格计算、订单生成等逻辑 } } ``` 3. **数据访问层(DAO/Repository)** - 封装数据库操作 - 集成MyBatis/JPA等ORM框架 - 示例JPA仓储接口: ```java public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); } ``` #### 二、核心业务模块划分 | 模块名称 | 功能描述 | 技术实现要点 | |---------------|------------------------------|---------------------------| | 用户中心 | 注册/登录/权限管理 | JWT+Spring Security | | 商品管理 | SPU/SKU管理、类目体系 | 树形结构存储+ES检索 | | 订单系统 | 订单生成/状态机/售后 | 分布式事务(Seata) | | 支付中心 | 支付渠道对接 | 策略模式+工厂模式 | | 库存服务 | 库存扣减/预占 | Redis分布式锁+最终一致性 | | 营销系统 | 优惠券/满减/秒杀活动 | Redis+Lua脚本实现原子操作 | #### 三、基础设施模块 1. **通用组件** - 认证鉴权模块:OAuth2.0+RBAC权限模型 - 网关层:Spring Cloud Gateway实现路由/限流 - 配置中心:Nacos统一管理配置 - 监控系统:Prometheus+Grafana监控体系 2. **消息队列应用** ```java // 订单创建后发送MQ消息 @Async public void asyncSendOrderEvent(Order order) { rabbitTemplate.convertAndSend( "order-exchange", "order.created", new OrderEvent(order) ); } ``` #### 四、目录结构示例 ``` src/main/java ├── config/ // 配置类 ├── controller/ // 接口层 ├── service/ // 业务服务层 │ ├── impl/ // 服务实现 ├── dao/ // 数据访问层 ├── model/ // 数据实体 │ ├── dto/ // 数据传输对象 │ ├── vo/ // 视图对象 ├── common/ // 公共组件 │ ├── exception/ // 异常处理 │ ├── utils/ // 工具类 │ ├── constants/ // 常量定义 └── mq/ // 消息队列处理 ``` #### 五、数据库设计原则 1. 用户表采用分库分表策略,通过`user_id%64`分片 2. 商品信息使用Elasticsearch建立搜索索引 3. 订单表按创建时间进行按月分表 4. 读写分离配置: ```yaml spring: datasource: write: url: jdbc:mysql://master:3306/mall read: - url: jdbc:mysql://slave1:3306/mall - url: jdbc:mysql://slave2:3306/mall ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值