秒杀微服务实现抢购代金券功能
Posted 共饮一杯无
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀微服务实现抢购代金券功能相关的知识,希望对你有一定的参考价值。
文章目录
需求分析
现在日常购物或者餐饮消费,商家经常会有推出代金券功能,有些时候代金券的数量不多是需要抢购的,那么怎么设计可以保证代金券的消耗量和秒杀到的用户保持一致呢?怎么设计可以保证一个用户只能秒杀到一张代金券呢?
秒杀场景的解决方案
秒杀场景有以下几个特点:
- 大量用户同时进行抢购操作,系统流量激增,服务器瞬时压力很大;
- 请求数量远大于商品库存量,只有少数客户可以成功抢购;
- 业务流程不复杂,核心功能是下订单。
秒杀场景的应对,一般要从以下几个方面进行处理,如下:
限流
:从客户端层面考虑,限制单个客户抢购频率;服务端层面,加强校验,识别请求是否来源于真实的客户端,并限制请求频率,防止恶意刷单;应用层面,可以使用漏桶算法或令牌桶算法实现应用级限流。缓存
:热点数据都从缓存获得,尽可能减小数据库的访问压力;异步
:客户抢购成功后立即返回响应,之后通过消息队列,异步处理后续步骤,如发短信、更新数据库等,从而缓解服务器峰值压力。分流
:单台服务器肯定无法应对抢购期间大量请求造成的压力,需要集群部署服务器,通过负载均衡共同处理客户端请求,分散压力。
数据库表设计
本文以抢购代金券为例,来进行数据库表的设计。
代金券表
CREATE TABLE `t_voucher` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '代金券标题',
`thumbnail` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '缩略图',
`amount` int(11) NULL DEFAULT NULL COMMENT '抵扣金额',
`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '售价',
`status` int(10) NULL DEFAULT NULL COMMENT '-1=过期 0=下架 1=上架',
`expire_time` datetime(0) NULL DEFAULT NULL COMMENT '过期时间',
`redeem_restaurant_id` int(10) NULL DEFAULT NULL COMMENT '验证餐厅',
`stock` int(11) NULL DEFAULT 0 COMMENT '库存',
`stock_left` int(11) NULL DEFAULT 0 COMMENT '剩余数量',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述信息',
`clause` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '使用条款',
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
`is_valid` tinyint(1) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
抢购活动表
CREATE TABLE `t_seckill_vouchers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fk_voucher_id` int(11) NULL DEFAULT NULL,
`amount` int(11) NULL DEFAULT NULL,
`start_time` datetime(0) NULL DEFAULT NULL,
`end_time` datetime(0) NULL DEFAULT NULL,
`is_valid` int(11) NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
订单表
CREATE TABLE `t_voucher_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` int(11) NULL DEFAULT NULL,
`fk_voucher_id` int(11) NULL DEFAULT NULL,
`fk_diner_id` int(11) NULL DEFAULT NULL,
`qrcode` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图片地址',
`payment` tinyint(4) NULL DEFAULT NULL COMMENT '0=微信支付 1=支付宝支付',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
`fk_seckill_id` int(11) NULL DEFAULT NULL COMMENT '如果是抢购订单时,抢购订单的id',
`order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
`is_valid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
创建秒杀服务
pom依赖
引入相关依赖如下:
<dependencies>
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- spring data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons -->
<dependency>
<groupId>com.zjq</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
</dependencies>
配置文件
server:
port: 7003 # 端口
spring:
application:
name: ms-seckill # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: localhost
timeout: 3000
password: 123456
# Swagger
swagger:
base-package: com.zjq.seckill
title: 秒杀微服务API接口文档
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true
instance-id: $spring.cloud.client.ip-address:$server.port
client:
service-url:
defaultZone: http://localhost:8080/eureka/
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰映射
service:
name:
ms-oauth-server: http://ms-oauth2-server/
logging:
pattern:
console: '%dHH:mm:ss [%thread] %-5level %logger50 - %msg%n'
关系型数据库实现代金券秒杀
相关实体引入
抢购代金券活动信息
代金券订单信息
Rest配置类
/**
* RestTemplate 配置类
* @author zjq
*/
@Configuration
public class RestTemplateConfiguration
@LoadBalanced
@Bean
public RestTemplate restTemplate()
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
restTemplate.getMessageConverters().add(converter);
return restTemplate;
全局异常处理
/**
*
* 全局异常处理类
* @author zjq
*/
// 将输出的内容写入 ResponseBody 中
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler
@Resource
private HttpServletRequest request;
@ExceptionHandler(ParameterException.class)
public ResultInfo<Map<String, String>> handlerParameterException(ParameterException ex)
String path = request.getRequestURI();
ResultInfo<Map<String, String>> resultInfo =
ResultInfoUtil.buildError(ex.getErrorCode(), ex.getMessage(), path);
return resultInfo;
@ExceptionHandler(Exception.class)
public ResultInfo<Map<String, String>> handlerException(Exception ex)
log.info("未知异常:", ex);
String path = request.getRequestURI();
ResultInfo<Map<String, String>> resultInfo =
ResultInfoUtil.buildError(path);
return resultInfo;
添加代金券秒杀活动
代金券活动实体
上述已引入实体。
代金券活动Mapper->SeckillVouchersMapper
/**
* 秒杀代金券 Mapper
* @author zjq
*/
public interface SeckillVouchersMapper
/**
* 新增秒杀活动
* @param seckillVouchers 代金券实体
* @return
*/
@Insert("insert into t_seckill_vouchers (fk_voucher_id, amount, start_time, end_time, is_valid, create_date, update_date) " +
" values (#fkVoucherId, #amount, #startTime, #endTime, 1, now(), now())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int save(SeckillVouchers seckillVouchers);
/**
* 根据代金券 ID 查询该代金券是否参与抢购活动
* @param voucherId 代金券id
* @return
*/
@Select("select id, fk_voucher_id, amount, start_time, end_time, is_valid " +
" from t_seckill_vouchers where fk_voucher_id = #voucherId")
SeckillVouchers selectVoucher(Integer voucherId);
代金券活动Service->SeckillService
/**
* 秒杀业务逻辑层
* @author zjq
*/
@Service
public class SeckillService
@Resource
private SeckillVouchersMapper seckillVouchersMapper;
/**
* 添加需要抢购的代金券
*
* @param seckillVouchers
*/
@Transactional(rollbackFor = Exception.class)
public void addSeckillVouchers(SeckillVouchers seckillVouchers)
// 非空校验
AssertUtil.isTrue(seckillVouchers.getFkVoucherId() == null, "请选择需要抢购的代金券");
AssertUtil.isTrue(seckillVouchers.getAmount() == 0, "请输入抢购总数量");
Date now = new Date();
AssertUtil.isNotNull(seckillVouchers.getStartTime(), "请输入开始时间");
// 生产环境下面一行代码需放行,这里注释方便测试
// AssertUtil.isTrue(now.after(seckillVouchers.getStartTime()), "开始时间不能早于当前时间");
AssertUtil.isNotNull(seckillVouchers.getEndTime(), "请输入结束时间");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "结束时间不能早于当前时间");
AssertUtil.isTrue(seckillVouchers.getStartTime().after(seckillVouchers.getEndTime()), "开始时间不能晚于结束时间");
// 验证数据库中是否已经存在该券的秒杀活动
SeckillVouchers seckillVouchersFromDb = seckillVouchersMapper.selectVoucher(seckillVouchers.getFkVoucherId());
AssertUtil.isTrue(seckillVouchersFromDb != null, "该券已经拥有了抢购活动");
// 插入数据库
seckillVouchersMapper.save(seckillVouchers);
验证数据库表 t_seckill_vouchers 中是否已经存在该券的秒杀活动:
- 如果存在则抛出异常;
- 如果不存在则将添加一个代金券抢购活动到 t_seckill_vouchers 表中;
代金券活动Controller->SeckillController
在网关微服务中配置秒杀服务路由和白名单方向
spring:
application:
name: ms-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启配置注册中心进行路由功能
lower-case-service-id: true # 将服务名称转小写
routes:
- id: ms-seckill
uri: lb://ms-seckill
predicates:
- Path=/seckill/**
filters:
- StripPrefix=1
secure:
ignore:
urls: # 配置白名单路径
# 内部配置所以放行
- /seckill/add
接口测试
对抢购的代金券下单
SeckillController
/**
* 秒杀下单
*
* @param voucherId 代金券id
* @param access_token 请求token
* @return
*/
@PostMapping("voucherId")
public ResultInfo<String> doSeckill(@PathVariable Integer voucherId, String access_token)
ResultInfo resultInfo = seckillService.doSeckill(voucherId,以上是关于秒杀微服务实现抢购代金券功能的主要内容,如果未能解决你的问题,请参考以下文章