背景
分布式微服务中的秒杀是怎么实现的呢?接着看下去吧
我们实现一个秒杀微服务,流程逻辑如下
项目搭建
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的库存也清零了
项目代码
项目代码