美文网首页大数据 爬虫Python AI Sql
打造一款抢全网红包现金券脚本!抢了两万个红包!Python也能实

打造一款抢全网红包现金券脚本!抢了两万个红包!Python也能实

作者: 1a076099f916 | 来源:发表于2018-10-13 15:10 被阅读10次

概述

电商的秒杀、抢购,春运抢票,微信QQ抢红包,从技术的角度来说,这对于Web 系统是一个很大的考验. 高并发场景下,系统的优化和稳定是至关重要的.

互联网的开发包括 Java 后台、 NoSQL、数据库、限流、CDN、负载均衡等内容, 目前并没有权威性的技术和设计,有的只是长期经验的总结,但是使用这些经验可以有效优化系统,提高系统的并发能力.

我们接下来的几篇博文主要讨论 Java 后台、 NoSQL ( Redis )和数据库部分技术.

抢红包案例

主要分以下几大部分:

环境搭建

模拟超量发送的场景-DataBase(MySql5.7)

悲观锁的实现版本-DataBase(MySql5.7)

乐观锁的实现版本-DataBase(MySql5.7)

Redis实现抢红包

案例关注点

模拟 20 万元的红包,共分为 2 万个可抢的小红包,有 3 万人同时抢夺的场景 ,模拟出现超发和如何保证数据一致性的问题。

在高并发的场景下,除了数据的一致性外,还要关注性能的问题 , 因为一般而言 , 时间太长用户体验就会很差,所以要测试数据一致性系统的性能 。

工程结构

库表设计

MySql5.7

/*==============================================================*/

/* Table: 红包表 */

/*==============================================================*/

create table T_RED_PACKET

(

id int(12) not null auto_increment COMMENT '红包编号',

user_id int(12) not null COMMENT '发红包的用户id',

amount decimal(16,2) not null COMMENT '红包金额',

send_date timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '发红包日期',

total int(12) not null COMMENT '红包总数',

unit_amount decimal(12) not null COMMENT '单个红包的金额',

stock int(12) not null COMMENT '红包剩余个数',

version int(12) default 0 not null COMMENT '版本(为后续扩展用)',

note varchar(256) null COMMENT '备注',,

primary key clustered (id)

);

红包表表示存放红包的是一个大红包的信息,它会分为若干个小红包,为了业务简单,假设每一个红包是等额的。而对于抢红包而言,就是从大红包中抢夺那些剩余的小红包,剩余红包数会被记录在红包表中。 两个表有外键关联 T_RED_PACKET.id = T_USER_RED_PACKET.red_packet_id

/*==============================================================*/

/* Table: 用户抢红包表 */

/*==============================================================*/

create table T_USER_RED_PACKET

(

id int(12) not null auto_increment COMMENT '用户抢到的红包id',

red_packet_id int(12) not null COMMENT '红包id',

user_id int(12) not null COMMENT '抢红包用户的id',

amount decimal(16,2) not null COMMENT '抢到的红包金额',

grab_time timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '抢红包时间',

note varchar(256) null COMMENT '备注',

primary key clustered (id)

);

/**

* 插入一个20万元金额,2万个小红包,每个10元的红包数据

*/

insert into T_RED_PACKET(user_id, amount, send_date, total, unit_amount, stock, note)

values(1, 200000.00, now(), 20000, 10.00, 20000,'20万元金额,2万个小红包,每个10元');

commit;

这样就建好了两个表,并且将一个 20 万元金额,2 万个小红包,每个 10 元的红包信息插入到了红包表中,用作模拟数据。

Domain

有了这两个表,我们就可以为这两个表建两个 POJO 了,让这两个表和 POJO 对应起来,这两个 POJO 为 RedPacket 和 UserRedPacket,实现类序列化接口。

红包信息

package com.artisan.redpacket.pojo;

import java.io.Serializable;

import java.sql.Timestamp;

/**

*

*

* @ClassName: RedPacket

*

* @Description: 红包表对应的实体类,可序列化

*

* @author: Mr.Yang

*

* @date: 2018年10月8日 下午3:42:58

*/

public class RedPacket implements Serializable {

private static final long serialVersionUID = 9036484563091364939L;

// 红包编号

private Long id;

// 发红包的用户id

private Long userId;

// 红包金额

private Double amount;

// 发红包日期

private Timestamp sendDate;

// 红包总数

private Integer total;

// 单个红包的金额

private Double unitAmount;

// 红包剩余个数

private Integer stock;

// 版本(为后续扩展用)

private Integer version;

// 备注

private String note;

// 省略set/get

}

抢红包信息

package com.artisan.redpacket.pojo;

import java.io.Serializable;

import java.sql.Timestamp;

/**

*

*

* @ClassName: UserRedPacket

*

* @Description: 用户抢红包表

*

* @author: Mr.Yang

*

* @date: 2018年10月8日 下午3:47:40

*/

public class UserRedPacket implements Serializable {

private static final long serialVersionUID = 7049215937937620886L;

// 用户红包id

private Long id;

// 红包id

private Long redPacketId;

// 抢红包的用户的id

private Long userId;

// 抢红包金额

private Double amount;

// 抢红包时间

private Timestamp grabTime;

// 备注

private String note;

// 省略set/get

}

Dao层实现

MyBatis Dao接口类及对应的Mapper文件

使用 MyBatis 开发,先来完成大红包信息的查询先来定义一个 DAO 对象

package com.artisan.redpacket.dao;

import org.springframework.stereotype.Repository;

import com.artisan.redpacket.pojo.RedPacket;

@Repository

public interface RedPacketDao {

/**

* 获取红包信息.

* @param id --红包id

* @return 红包具体信息

*/

public RedPacket getRedPacket(Long id);

/**

* 扣减抢红包数.

* @param id -- 红包id

* @return 更新记录条数

*/

public int decreaseRedPacket(Long id);

}

其中的两个方法 , 一个是查询红包,另一个是扣减红包库存。

抢红包的逻辑是,先查询红包的信息,看其是否拥有存量可以扣减。如果有存量,那么可以扣减它,否则就不扣减。

接着将对应的Mapper映射文件编写一下

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

resultType="com.artisan.redpacket.pojo.RedPacket">

select id, user_id as userId, amount, send_date as

sendDate, total,

unit_amount as unitAmount, stock, version, note from

T_RED_PACKET

where id = #{id}

update T_RED_PACKET set stock = stock - 1 where id =

#{id}

这里getRedPacket并没有加锁这类动作,目的是为了演示超发红包的情况.

然后是抢红包的设计了 ,先来定义插入抢红包的 DAO ,紧接着是Mapper映射文件

package com.artisan.redpacket.dao;

import org.springframework.stereotype.Repository;

import com.artisan.redpacket.pojo.UserRedPacket;

@Repository

public interface UserRedPacketDao {

/**

* 插入抢红包信息.

* @param userRedPacket ——抢红包信息

* @return 影响记录数.

*/

public int grapRedPacket(UserRedPacket userRedPacket);

}

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

keyProperty="id" parameterType="com.artisan.redpacket.pojo.UserRedPacket">

insert into T_USER_RED_PACKET( red_packet_id, user_id, amount, grab_time, note)

values (#{redPacketId}, #{userId}, #{amount}, now(), #{note})

这里使用了 useGeneratedKeys 和 keyPrope町,这就意味着会返回数据库生成的主键信息,这样就可以拿到插入记录的主键了 , 关于 DAO 层就基本完成了。别忘了单元测试!!!

Service层实现

接下来定义两个 Service 层接口,分别是 UserRedPacketService和RedPacketService

package com.artisan.redpacket.service;

import com.artisan.redpacket.pojo.RedPacket;

public interface RedPacketService {

/**

* 获取红包

* @param id ——编号

* @return 红包信息

*/

public RedPacket getRedPacket(Long id);

/**

* 扣减红包

* @param id——编号

* @return 影响条数.

*/

public int decreaseRedPacket(Long id);

}

package com.artisan.redpacket.service;

public interface UserRedPacketService {

/**

* 保存抢红包信息.

* @param redPacketId 红包编号

* @param userId 抢红包用户编号

* @return 影响记录数.

*/

public int grapRedPacket(Long redPacketId, Long userId);

}

实现类如下:

package com.artisan.redpacket.service.impl;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Isolation;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

import com.artisan.redpacket.dao.RedPacketDao;

import com.artisan.redpacket.pojo.RedPacket;

import com.artisan.redpacket.service.RedPacketService;

@Service

public class RedPacketServiceImpl implements RedPacketService {

@Autowired

private RedPacketDao redPacketDao;

@Override

@Transactional(isolation=Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public RedPacket getRedPacket(Long id) {

return redPacketDao.getRedPacket(id);

}

@Override

@Transactional(isolation=Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public int decreaseRedPacket(Long id) {

return redPacketDao.decreaseRedPacket(id);

}

}

配置了事务注解@Transactional , 让程序能够在事务中运行,以保证数据的一致性 , 这里采用的是读/写提交的隔离级别 , 之所以不采用更高的级别, 主要是提高数据库的并发能力,而对于传播行为则采用 Propagation.REQUIRED,这样调用这个方法的时候,如果没有事务则会创建事务, 如果有事务则沿用当前事务。

实现 UserRedPacketService 接口的方法 grapRedPacket,它是核心的接口方法

package com.artisan.redpacket.service.impl;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Isolation;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

import com.artisan.redpacket.dao.RedPacketDao;

import com.artisan.redpacket.dao.UserRedPacketDao;

import com.artisan.redpacket.pojo.RedPacket;

import com.artisan.redpacket.pojo.UserRedPacket;

import com.artisan.redpacket.service.UserRedPacketService;

@Service

public class UserRedPacketServiceImpl implements UserRedPacketService {

// private Logger logger =

// LoggerFactory.getLogger(UserRedPacketServiceImpl.class);

@Autowired

private UserRedPacketDao userRedPacketDao;

@Autowired

private RedPacketDao redPacketDao;

// 失败

private static final int FAILED = 0;

@Override

@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public int grapRedPacket(Long redPacketId, Long userId) {

// 获取红包信息

RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);

int leftRedPacket = redPacket.getStock();

// 当前小红包库存大于0

if (leftRedPacket > 0) {

redPacketDao.decreaseRedPacket(redPacketId);

// logger.info("剩余Stock数量:{}", leftRedPacket);

// 生成抢红包信息

UserRedPacket userRedPacket = new UserRedPacket();

userRedPacket.setRedPacketId(redPacketId);

userRedPacket.setUserId(userId);

userRedPacket.setAmount(redPacket.getUnitAmount());

userRedPacket.setNote("redpacket- " + redPacketId);

// 插入抢红包信息

int result = userRedPacketDao.grapRedPacket(userRedPacket);

return result;

}

// logger.info("没有红包啦.....剩余Stock数量:{}", leftRedPacket);

// 失败返回

return FAILED;

}

}

grapRedPacket 方法的逻辑是首先获取红包信息,如果发现红包库存大于 0,则说明还有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。要注意的是,数据库事务方面的设置,代码中使用注解@Transactional , 说明它会在一个事务中运行,这样就能够保证所有的操作都是在一个事务中完成的。在高并发中会发生超发的现象,后面会看到超发的实际测试。

使用全注解搭建SSM 开发环境

我们这里将使用注解的方式来完成 SSM 开发的环境,可以通过继承 AbstractAnnotationConfigDispatcherServletlnitfal izer 去配置其他内 容,因此首先来配置 WebApplnitialize

package com.artisan.redpacket.config;

import javax.servlet.MultipartConfigElement;

import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

// Spring IoC环境配置

@Override

protected Class[] getRootConfigClasses() {

// 配置Spring IoC资源

return new Class[] { RootConfig.class };

}

// DispatcherServlet环境配置

@Override

protected Class[] getServletConfigClasses() {

// 加载Java配置类

return new Class[] { WebConfig.class };

}

// DispatchServlet拦截请求配置

@Override

protected String[] getServletMappings() {

return new String[] { "*.do" };

}

/**

* @param dynamic

* Servlet上传文件配置.

*/

@Override

protected void customizeRegistration(Dynamic dynamic) {

// 配置上传文件路径

String filepath = "D:/";

// 5MB

Long singleMax = (long) (5 * Math.pow(2, 20));

// 10MB

Long totalMax = (long) (10 * Math.pow(2, 20));

// 设置上传文件配置

dynamic.setMultipartConfig(new MultipartConfigElement(filepath, singleMax, totalMax, 0));

}

}

WebAppInitializer继承了 AbstractAnnotationConfigDispatcherServletlnitializer, 重写了 3 个抽象方法 , 并且覆盖了父类的 customizeRegistration 方法 , 作为上传文件的配置。

getRootConfigClasses 是一个配置 Spring IoC 容器的上下文配置 , 此配置在代码中将会由类 RootConfig 完成

getServletConfigClasses 配置 DispatcherServlet 上下文配置,将会由WebConfig完成

getServletMappings 配置 DispatcherServlet 拦截 内 容 , 这里设置的是拦截所有以 .do 结尾的请求

通过这 3 个方法就可以配置 Web 工程中 的 Spring IoC 资源和 DispatcherServlet 的配置内容 , 首先是配置 Spring IoC 容器,配置类 RootConfig

package com.artisan.redpacket.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;

import org.mybatis.spring.SqlSessionFactoryBean;

import org.mybatis.spring.mapper.MapperScannerConfigurer;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.ComponentScan.Filter;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.FilterType;

import org.springframework.core.io.ClassPathResource;

import org.springframework.core.io.Resource;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.stereotype.Repository;

import org.springframework.stereotype.Service;

import org.springframework.transaction.PlatformTransactionManager;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import org.springframework.transaction.annotation.TransactionManagementConfigurer;

@Configuration

//定义Spring 扫描的包

@ComponentScan(value= "com.*", includeFilters= {@Filter(type = FilterType.ANNOTATION, value ={Service.class})})

//使用事务驱动管理器

@EnableTransactionManagement

//实现接口TransactionManagementConfigurer,这样可以配置注解驱动事务

public class RootConfig implements TransactionManagementConfigurer {

private DataSource dataSource = null;

/**

* 配置数据库.

* @return 数据连接池

*/

@Bean(name = "dataSource")

public DataSource initDataSource() {

if (dataSource != null) {

return dataSource;

}

try {

Properties props = new Properties();

props.load(RootConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"));

props.setProperty("driverClassName", props.getProperty("jdbc.driver"));

props.setProperty("url", props.getProperty("jdbc.url"));

props.setProperty("username", props.getProperty("jdbc.username"));

props.setProperty("password", props.getProperty("jdbc.password"));

dataSource = BasicDataSourceFactory.createDataSource(props);

} catch (Exception e) {

e.printStackTrace();

}

return dataSource;

}

/***

* 配置SqlSessionFactoryBean

* @return SqlSessionFactoryBean

*/

@Bean(name="sqlSessionFactory")

public SqlSessionFactoryBean initSqlSessionFactory() {

SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();

sqlSessionFactory.setDataSource(initDataSource());

//配置MyBatis配置文件

Resource resource = new ClassPathResource("mybatis/mybatis-config.xml");

sqlSessionFactory.setConfigLocation(resource);

return sqlSessionFactory;

}

/***

* 通过自动扫描,发现MyBatis Mapper接口

* @return Mapper扫描器

*/

@Bean

public MapperScannerConfigurer initMapperScannerConfigurer() {

MapperScannerConfigurer msc = new MapperScannerConfigurer();

msc.setBasePackage("com.*");

msc.setSqlSessionFactoryBeanName("sqlSessionFactory");

msc.setAnnotationClass(Repository.class);

return msc;

}

/**

* 实现接口方法,注册注解事务,当@Transactional 使用的时候产生数据库事务

*/

@Override

@Bean(name="annotationDrivenTransactionManager")

public PlatformTransactionManager annotationDrivenTransactionManager() {

DataSourceTransactionManager transactionManager =

new DataSourceTransactionManager();

transactionManager.setDataSource(initDataSource());

return transactionManager;

}

}

这个类和之前论述的有所不同 , 它标注了注解@EnableTransactionManagement , 实现了接口 TransactionManagementConfigurer, 这样的配置是为了实现注解式的事务 , 将来可以通过注解@Transactional 配 置数据库事务。

它有一 个方法annotationDrivenTransactionManager这需要将一个事务管理器返回给它就可以了

除了配置数据库事务外 ,还配置了数据源 SqISessionFactoryBean 和 MyBatis 的扫描类 , 并把 MyBatis的扫描类通过注解@Repository 和包名"com.*"限定。这样 MyBatis 就会通过 Spring 的机制找到对应的接 口和配置 , Spring 会自动把对应的接口装配到 IoC 容器中 。

有了 Spring IoC 容器后 , 还需要配置 DispatcherServlet 上下文

package com.artisan.redpacket.config;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.ComponentScan.Filter;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.FilterType;

import org.springframework.http.MediaType;

import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import org.springframework.scheduling.annotation.AsyncConfigurerSupport;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import org.springframework.stereotype.Controller;

import org.springframework.web.servlet.HandlerAdapter;

import org.springframework.web.servlet.ViewResolver;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration

//定义Spring MVC扫描的包

@ComponentScan(value="com.*", includeFilters= {@Filter(type = FilterType.ANNOTATION, value = Controller.class)})

//启动Spring MVC配置

@EnableWebMvc

public class WebConfig{

/***

* 通过注解 @Bean 初始化视图解析器

* @return ViewResolver 视图解析器

*/

@Bean(name="internalResourceViewResolver")

public ViewResolver initViewResolver() {

InternalResourceViewResolver viewResolver =new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/jsp/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}

/**

* 初始化RequestMappingHandlerAdapter,并加载Http的Json转换器

* @return RequestMappingHandlerAdapter 对象

*/

@Bean(name="requestMappingHandlerAdapter")

public HandlerAdapter initRequestMappingHandlerAdapter() {

//创建RequestMappingHandlerAdapter适配器

RequestMappingHandlerAdapter rmhd = new RequestMappingHandlerAdapter();

//HTTP JSON转换器

MappingJackson2HttpMessageConverter jsonConverter

= new MappingJackson2HttpMessageConverter();

//MappingJackson2HttpMessageConverter接收JSON类型消息的转换

MediaType mediaType = MediaType.APPLICATION_JSON_UTF8;

List mediaTypes = new ArrayList();

mediaTypes.add(mediaType);

//加入转换器的支持类型

jsonConverter.setSupportedMediaTypes(mediaTypes);

//往适配器加入json转换器

rmhd.getMessageConverters().add(jsonConverter);

return rmhd;

}

}

这里配置了一个视图解析器 , 通过它找到对应 JSP 文件,然后使用数据模型进行渲染,采用自定义 创 建 RequestMappingHandlerAdapter , 为了让它能够支持 JSON 格式(@ResponseBody ) 的转换,所以需要创建一个关于对象和 JSON 的转换消息类MappingJackson2HttpMessageConverter

创建它之后,把它注册给 RequestMappingHandlerAdapter对象 , 这样当控制器遇到注解@ResponseBody 的时候就知道采用 JSON 消息类型进行应答 , 那么在控制器完成逻辑后 , 由处理器将其和消息转换类型做匹配,找到MappingJackson2HttpMessageConverter 类对象,从而转变为 JSON 数据。

通过上面的 3 个类就搭建好了 Spring MVC 和 Spring 的开发环境,但是没有完成对MyBatis 配置文件. 从RootConfig#initSqlSessionFactory()方法中看到加载的MyBatis 的配置文件为"mybatis/mybatis-config.xml",该配置文件主要是加载mapper映射文件

PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-config.dtd">

记得进行Service层的单元测试, 关于后台的逻辑就已经完成 , 接下来继续将Controller层实现,进行页面测试吧。

Controller层

package com.artisan.redpacket.controller;

import java.util.HashMap;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import com.artisan.redpacket.service.UserRedPacketService;

@Controller

@RequestMapping("/userRedPacket")

public class UserRedPacketController {

@Autowired

private UserRedPacketService userRedPacketService;

@RequestMapping(value = "/grapRedPacket")

@ResponseBody

public Map grapRedPacket(Long redPacketId, Long userId) {

// 抢红包

int result = userRedPacketService.grapRedPacket(redPacketId, userId);

Map retMap = new HashMap();

boolean flag = result > 0;

retMap.put("success", flag);

retMap.put("message", flag ? "抢红包成功" : "抢红包失败");

return retMap;

}

}

对于控制器而言 , 它将抢夺一个红包 , 并且将一个 Map返回,由于使用了注解@ResponseBody 标注方法,所以最后它会转变为一个 JSON 返回给前端请求,编写 JSP 对其进行测试

View层

grap.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

参数

$(document).ready(function () {

//模拟30000个异步请求,进行并发

var max = 30000;

for (var i = 1; i <= max; i++) {

//jQuery的post请求,请注意这是异步请求

$.post({

//请求抢id为1的红包

//根据自己请求修改对应的url和大红包编号

url: "./userRedPacket/grapRedPacket.do?redPacketId=1&userId=" + i,

//成功后的方法

success: function (result) {

}

});

}

});

这里我们使用了 JavaScript 去模拟 3 万人同时抢红包的场景 . 请使用 Firefox进行测试(Chrome老是丢失请求,IE慢)

JavaScript 的 post 请求是一个异步请求,所以这是一个高并发的场景,它将抢夺 id 为1的红包 , 依据之前 SQL 的插入 , 这是一个 20 万元的红包 , 一共有两万个,那么在这样高并发场景下会有什么问题发生呢?

注意两个点 : 一个是数据的一致性,另外一个是性能问题

运行测试

启动tomcat,前端访问 http://localhost:8080/ssm_redpacket/grap.jsp

如果有日志,记得调成error级别,或者不打印日志。

我这里的mysql是部署在虚拟机中,CPU和内存的配置都不高。 内存1G。

超量发送的BUG验证

模拟高并发场景的抢红包后,两个维度进行统计

1:数据一致性

2: 性能

抢红包一致性统计:

SELECT

a.id,

a.amount,

a.stock

FROM

T_RED_PACKET a

WHERE

a.id = 1

UNION ALL

SELECT

max(b.user_id),

sum(b.amount),

count(*)

FROM

T_USER_RED_PACKET b

WHERE

b.red_packet_id = 1;

使用 SQL 去查询红包的库存、发放红包的总个数、总金额,我们发现了错误,红包总额为 20 万元,两万个小红包,结果发放了 200020元的红包, 20002 个红包。现有库存为-2,超出了之前的限定,这就是高并发的超发现象,这是一个错误的逻辑 。

抢红包性能统计:

SELECT

(

UNIX_TIMESTAMP(max(a.grab_time)) - UNIX_TIMESTAMP(min(a.grab_time))

) AS lastTime

FROM

T_USER_RED_PACKET a;

一共使用了 190 秒的时间,完成 20002 个红包的抢夺,性能一般。。。但是逻辑上存在超发错误,还需要解决超发问题 。

文末,加小编群QQ群:865597862可以获取更多学习资料哦!

相关文章

网友评论

    本文标题:打造一款抢全网红包现金券脚本!抢了两万个红包!Python也能实

    本文链接:https://www.haomeiwen.com/subject/mapoaftx.html