当前位置: 首页 > news >正文

SpringCloudAlibaba【六】微服务架构下的秒杀案例

背景

分布式微服务中的秒杀是怎么实现的呢?接着看下去吧

我们实现一个秒杀微服务,流程逻辑如下

在这里插入图片描述

项目搭建

MySQL

create database if not exists demo;

use demo;

drop table if exists skill_goods;

create table if not exists skill_goods (
    id bigint(20) key,
    name varchar(200),
    price decimal(10,2),
    cost_price decimal(10,2),
    status char,
    num int,
    stock_count int,
    introduction varchar(200)
);

drop table if exists skill_order;

create table if not exists skill_order (
    id bigint key,
    skill_id bigint,
    money decimal,
    user_id varchar(20),
    create_time datetime,
    pay_time datetime,
    status char
);

在这里插入图片描述

在这里插入图片描述

IDEA中构建Maven父项目,再建两个子项目Product和Skill

在这里插入图片描述

POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springcloud-nacos</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>skill</module>
        <module>product</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <!--add-->

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.itlym.shoulder</groupId>
            <artifactId>lombok</artifactId>
            <version>0.1</version>
        </dependency>

        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.69</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.5.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

Product模块

创建如下目录结构

在这里插入图片描述

YML

server:
  port: 9000
spring:
  cloud:
    nacos:
      discovery:
        service: prod-serv
      server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: fengzhizi98!

  redis:
    host: localhost
    port: 6379

ProductApplication

package com.product;

import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ProductApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class);
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer());
        return template;
    }

}

ProductController

package com.product.controller;

import com.product.entity.Goods;
import com.product.service.GoodService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
public class ProductController {

    @Resource
    private GoodService goodService;

    @GetMapping("/product/{productId}")
    public Goods product(@PathVariable Long productId) {
        System.out.println("调用商品服务");
        return goodService.queryGoods(productId);
    }

    @PostMapping("/product")
    public String update(@RequestBody Goods goods) {
        goodService.update(goods);
        return "更新库存成功";
    }

}

GoodService

package com.product.service;

import com.alibaba.fastjson.JSON;
import com.product.dao.GoodsDao;
import com.product.entity.Goods;
import com.product.utils.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Component
public class GoodService {

    public static final String SKILL_GOODS_PHONE = "SKILL_GOODS_PHONE";
    public static final String SKILL_GOODS_QUEUE = "SKILL_GOODS_QUEUE";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private GoodsDao goodsDao;

    @Scheduled(fixedDelay = 5000)
    public void prepareGood() {
        System.out.println("开始加载商品");

        Set<Long> set = redisTemplate.boundHashOps(SKILL_GOODS_PHONE).keys();
        List<Long> ids = new ArrayList<>();
        for (Long id : set) {
            ids.add(id);
        }

        List<Goods> list = null;
        //只查询出不在内存当中的商品信息,并加载到内存
        if (CollectionUtils.isEmpty(ids)) {
            list = goodsDao.findAllGoods();
        } else {
            list = goodsDao.findGoods(ids);
        }

        if (!CollectionUtils.isEmpty(list)) {
            for (Goods goods : list) {
                redisTemplate.boundHashOps(SKILL_GOODS_PHONE).put(goods.getId(), JSON.toJSONString(goods));
                redisTemplate.boundListOps(SKILL_GOODS_QUEUE+goods.getId()).leftPush(convertToArray(goods.getStock_count(), goods.getId()));
            }
        }

        //查看当前缓存中所有的商品信息
        Set keys = redisTemplate.boundHashOps(SKILL_GOODS_PHONE).keys();
        for (Object s : keys) {
            Goods goods = JSONUtil.toEntity((String) redisTemplate.boundHashOps(SKILL_GOODS_PHONE).get(s), Goods.class);
            System.out.println(goods.getName() + " 库存剩余:" + goods.getStock_count());
        }
    }

    private Long[] convertToArray(Integer stock_count, Long id) {
        Long[] idlong = new Long[stock_count];
        for (int i = 0; i < stock_count; i++) {
            idlong[i] = id;
        }
        return idlong;
    }

    //查询商品信息
    public Goods queryGoods (Long productId) {
        return JSONUtil.toEntity((String) redisTemplate.boundHashOps(SKILL_GOODS_PHONE).get(productId), Goods.class);
    }

    //更新商品信息
    public void update(Goods goods) {
        goodsDao.save(goods);
    }

}

GoodsDao

package com.product.dao;

import com.product.entity.Goods;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface GoodsDao extends JpaRepository<Goods, Long> {

    @Query(value = "select * from skill_goods where status = 1 and num > 0 and stock_count > 0 and id not in (?1)", nativeQuery = true)
    List<Goods> findGoods(List<Long> ids);

    @Query(value = "select * from skill_goods where status = 1 and num > 0 and stock_count > 0", nativeQuery = true)
    List<Goods> findAllGoods();

}

Goods

package com.product.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;

@Entity
@Table(name = "skill_goods")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "price")
    private BigDecimal price;

    @Column(name = "cost_price")
    private BigDecimal cost_price;

    @Column(name = "status")
    private String status;

    @Column(name = "num")
    private Integer num;

    @Column(name = "stock_count")
    private Integer stock_count;

    @Column(name = "introduction")
    private String introduction;

}

JSONUtil

说明:为了使Redis的序列化简化,我们使用JSON来存储对象信息,该工具主要功能使JSON和实体的转换

package com.product.utils;

import com.alibaba.fastjson.JSON;

public class JSONUtil {

    /***
     * 将JSON文本反序列化到对象
     */
    public static <T> T toEntity(String jsonString, Class<T> bean) {
        T t = (T) JSON.parseObject(jsonString, bean); // fastjson
        return t;
    }

}

Skill模块

创建如下目录结构

在这里插入图片描述

YML

server:
  port: 13000
spring:
  cloud:
    nacos:
      discovery:
        service: skill-serv
      server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: fengzhizi98!

  redis:
    host: localhost
    port: 6379

SkillApplication

package com.skill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableAsync
public class SkillApplication {

    public static void main(String[] args) {
        SpringApplication.run(SkillApplication.class);
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer());
        return template;
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }


}

SkillApplication

package com.skill.controller;

import com.skill.service.SkillGoodsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class SkillController {

    @Resource
    private SkillGoodsService skillGoodsService;

    @GetMapping("/skill")
    public String skill(String userId, Long productId) {
        try{
            skillGoodsService.add(productId, userId);
            return "秒杀成功";
        } catch (Exception e) {
            return e.getMessage();
        }
    }

}

SkillGoodsService

package com.skill.service;

import com.alibaba.fastjson.JSON;
import com.skill.entity.SkillEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.UUID;

@Service
public class SkillGoodsService {

    public static final String SKILL_GOODS_LIST = "SKILL_GOODS_LIST";

    public static final String SKILL_GOODS_ONLY = "SKILL_GOODS_ONLY";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private MultiThreadOrder multiThreadOrder;

    @Transactional
    public void add(Long productId, String userId) {

        //模拟多用户
        userId = UUID.randomUUID().toString();

        //判断用户是否参加过抢单
        Long time = redisTemplate.boundHashOps(SKILL_GOODS_ONLY).increment(userId, 1L);
        try {
            if (time > 1) {
                throw new Exception("重复抢单,不要贪心");
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        //封装对象放入Redis队列
        SkillEntity skill = new SkillEntity();
        skill.setProductId(productId);
        skill.setUserId(userId);

        redisTemplate.boundListOps(SKILL_GOODS_LIST).leftPush(JSON.toJSONString(skill));

        try {
            multiThreadOrder.createOrder();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

}

ProductService

package com.skill.service;

import com.skill.entity.Goods;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class ProductService {

    @Autowired
    private RestTemplate restTemplate;

    public Goods queryByProductId(Long productId) {
        return restTemplate.getForObject("http://prod-serv/product/" + productId, Goods.class);
    }

    public void update(Goods goods) {
        ResponseEntity<String> result = restTemplate.postForEntity("http://prod-serv/product", goods, String.class);
        System.out.println(result.getBody());
    }

}

MultiThreadOrder

package com.skill.service;

import com.skill.dao.SkillOrderDao;
import com.skill.entity.Goods;
import com.skill.entity.SkillEntity;
import com.skill.entity.SkillOrder;
import com.skill.utils.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class MultiThreadOrder {

    public static final String SKILL_GOODS_PHONE = "SKILL_GOODS_PHONE";

    public static final String SKILL_GOODS_LIST = "SKILL_GOODS_LIST";

    public static final String SKILL_GOODS_QUEUE = "SKILL_GOODS_QUEUE";

    private static final String SKILL_GOODS_ONLY = "SKILL_GOODS_ONLY";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SkillOrderDao skillOrderDao;

    @Autowired
    private ProductService productService;

    @Async
    public void createOrder() {
        System.out.println("开始异步抢单");

        SkillEntity skill = JSONUtil.toEntity((String) redisTemplate.boundListOps(SKILL_GOODS_LIST).rightPop(), SkillEntity.class);
        if (skill == null) {
            return;
        }
        Long productId = skill.getProductId();
        String userId = skill.getUserId();

        Goods goods = productService.queryByProductId(productId);

        try {
            if (goods == null) {
                throw new Exception("商品被抢光");
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        Long stockId = (Long) redisTemplate.boundListOps(SKILL_GOODS_QUEUE + productId).rightPop();
        if (stockId == null) {
            System.out.println("该商品已被秒杀完毕");
            redisTemplate.boundHashOps(SKILL_GOODS_ONLY).delete(userId);
            redisTemplate.boundHashOps(SKILL_GOODS_PHONE).delete(goods.getId());
            goods.setStock_count(0);
            productService.update(goods);
            return;
        }

        SkillOrder skillOrder = new SkillOrder();
        skillOrder.setMoney(goods.getCost_price());
        skillOrder.setPayTime(new Date());
        skillOrder.setStatus("0");
        skillOrder.setUserId(userId);
        skillOrder.setCreateTime(new Date());
        skillOrder.setSkillId(productId);

        skillOrderDao.save(skillOrder);

        System.out.println("结束异步抢单");
    }
}

SkillOrderDao

package com.skill.dao;

import com.skill.entity.SkillOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SkillOrderDao extends JpaRepository<SkillOrder, Long> {
}

Goods

package com.skill.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;

@Entity
@Table(name = "skill_goods")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "price")
    private BigDecimal price;

    @Column(name = "cost_price")
    private BigDecimal cost_price;

    @Column(name = "status")
    private String status;

    @Column(name = "num")
    private Integer num;

    @Column(name = "stock_count")
    private Integer stock_count;

    @Column(name = "introduction")
    private String introduction;

}

SkillEntitiy

package com.skill.entity;

import lombok.Data;

import java.io.Serializable;

@Data
public class SkillEntity implements Serializable {
    private Long productId;

    private String userId;
}

SkillOrder

package com.skill.entity;


import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

@Data
@Entity
@Table(name = "skill_order")
public class SkillOrder implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private String id;

    @Column(name = "skill_id")
    private Long skillId;

    @Column(name = "money")
    private BigDecimal money;

    @Column(name = "user_id")
    private String userId;

    @Column(name = "create_time")
    private Date createTime;

    @Column(name = "pay_time")
    private Date payTime;

    @Column(name = "status")
    private String status;
}

JSONUtil

package com.skill.utils;

import com.alibaba.fastjson.JSON;

public class JSONUtil {

    /***
     * 将JSON文本反序列化到对象
     */
    public static <T> T toEntity(String jsonString, Class<T> bean) {
        T t = (T) JSON.parseObject(jsonString, bean); // fastjson
        return t;
    }

}

测试

先启动Redis,Sentinel,MySQL,Nacos

Nacos

在这里插入图片描述

Sentinel

在这里插入图片描述

Redis

在这里插入图片描述

MySQL

在这里插入图片描述

启动项目

在这里插入图片描述

Apifox高并发测试

在这里插入图片描述

结果

IPHONE12 已经被抢光了

在这里插入图片描述

数据库IPHONE12的库存也清零了

在这里插入图片描述

项目代码

项目代码

相关文章:

  • 字节一面:TCP 三次握手,问的好细!
  • 一个功能齐全的,多用途管理后台模板
  • 【C语言】三子棋小游戏
  • Python自动化:Windows下不用任务管理器也可以轻松定时执行任务
  • 基于HTML美中华传统文化题材网页项目的设计与实现 (纯HTML+CSS制作中国茶文化网站)...
  • 网络请求+基于Node.js的WebSocket
  • 两例典型的C++软件异常排查实例分享
  • 第30讲:事务的基本概念以及如何实现事务
  • 【C++初阶】类和对象(二)
  • C++位图
  • 人生重开模拟器(Python实现)
  • 数据结构与算法----栈和队列(Stack Queue)
  • 零基础小白学Node-RED(06):网络功能
  • 巧用GitHub Action实现自动化部署Java项目
  • 【UI自动化】给好友发大批量图片
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • 【EOS】Cleos基础
  • 【React系列】如何构建React应用程序
  • 2018以太坊智能合约编程语言solidity的最佳IDEs
  • Android 控件背景颜色处理
  • in typeof instanceof ===这些运算符有什么作用
  • iOS | NSProxy
  • JavaScript类型识别
  • JavaScript学习总结——原型
  • Laravel Telescope:优雅的应用调试工具
  • leetcode388. Longest Absolute File Path
  • React16时代,该用什么姿势写 React ?
  • spring学习第二天
  • Webpack入门之遇到的那些坑,系列示例Demo
  • 得到一个数组中任意X个元素的所有组合 即C(n,m)
  • 观察者模式实现非直接耦合
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 坑!为什么View.startAnimation不起作用?
  • 学习HTTP相关知识笔记
  • 再谈express与koa的对比
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • #AngularJS#$sce.trustAsResourceUrl
  • #LLM入门|Prompt#2.3_对查询任务进行分类|意图分析_Classification
  • ( 用例图)定义了系统的功能需求,它是从系统的外部看系统功能,并不描述系统内部对功能的具体实现
  • (145)光线追踪距离场柔和阴影
  • (NSDate) 时间 (time )比较
  • (vue)页面文件上传获取:action地址
  • (第8天)保姆级 PL/SQL Developer 安装与配置
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (理论篇)httpmoudle和httphandler一览
  • (七)理解angular中的module和injector,即依赖注入
  • (算法)Game
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .NET CF命令行调试器MDbg入门(三) 进程控制
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .net 打包工具_pyinstaller打包的exe太大?你需要站在巨人的肩膀上-VC++才是王道
  • .Net 知识杂记