高并发-「抢红包案例」之一:SSM环境搭建及复现红包超发问题

栏目: Java · 发布时间: 5年前

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

文章目录

  • 概述

  • 抢红包案例

  • 案例关注点

  • 工程结构

  • 库表设计

  • Domain

  • Dao层实现

  • Service层实现

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

  • Controller层

  • View层

  • 运行测试

  • 超量发送的BUG验证

  • 超发问题解决思路

概述

高并发-「抢红包案例」之一:SSM环境搭建及复现红包超发问题

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

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

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

抢红包案例

主要分以下几大部分:

  1. 环境搭建

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

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

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

  5. Redis实现抢红包

案例关注点

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

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

工程结构

高并发-「抢红包案例」之一:SSM环境搭建及复现红包超发问题

库表设计

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映射文件编写一下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.artisan.redpacket.dao.RedPacketDao">
	<!-- 查询红包具体信息 -->
	<select id="getRedPacket" parameterType="long"
		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}
	</select>
	<!-- 扣减抢红包库存 -->
	<update id="decreaseRedPacket">
		update T_RED_PACKET set stock = stock - 1 where id =
		#{id}
	</update>
</mapper>

这里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);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.artisan.redpacket.dao.UserRedPacketDao">
 <!-- 插入抢红包信息 -->
 <insert id="grapRedPacket" useGeneratedKeys="true" 
 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}) 
 </insert>
</mapper>

这里使用了 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 extends AsyncConfigurerSupport { 
	/***
	 * 通过注解 @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<MediaType> mediaTypes = new ArrayList<MediaType>();
		mediaTypes.add(mediaType);
		//加入转换器的支持类型
		jsonConverter.setSupportedMediaTypes(mediaTypes);
		//往适配器加入json转换器
		rmhd.getMessageConverters().add(jsonConverter);
		return rmhd;
	}
	
	
	@Override
	public Executor getAsyncExecutor() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(5);
		taskExecutor.setMaxPoolSize(10);
		taskExecutor.setQueueCapacity(200);
		taskExecutor.initialize();
		return taskExecutor;
	}
}

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

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

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
 <mappers>
 <mapper resource="mapper/UserRedPacket.xml"/>
 <mapper resource="mapper/RedPacket.xml"/>
 </mappers>
</configuration>

记得进行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<String, Object> grapRedPacket(Long redPacketId, Long userId) {
		// 抢红包
		int result = userRedPacketService.grapRedPacket(redPacketId, userId);
		Map<String, Object> retMap = new HashMap<String, Object>();
		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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title>参数</title>
 <!-- 加载Query文件-->
 <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js">
 </script>
 <script type="text/javascript">
 $(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) {
 }
 });
 }
 });
 </script>
 </head>
 <body>
 </body>
</html>

这里我们使用了 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;

高并发-「抢红包案例」之一:SSM环境搭建及复现红包超发问题

使用 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;

高并发-「抢红包案例」之一:SSM环境搭建及复现红包超发问题

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

超发问题解决思路

超发现象是由多线程下数据不一致造成的,对于此类问题,如果采用数据库方案的话,主要通过悲观锁和乐观锁来处理,这两种方法的性能是不一样的。

接下来我们分别使用悲观锁、乐观锁、Redis+lua的方式来解决这个超发问题。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Ruby

Programming Ruby

Dave Thomas、Chad Fowler、Andy Hunt / Pragmatic Bookshelf / 2004-10-8 / USD 44.95

Ruby is an increasingly popular, fully object-oriented dynamic programming language, hailed by many practitioners as the finest and most useful language available today. When Ruby first burst onto the......一起来看看 《Programming Ruby》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具