模拟抢红包的场景(出现超发的现象)
1、开发环境
MyEclipse10、tomcat7、ssm(spring4、springmvc4、mybatis3)、mysql5
2、结构图
3、抢红包的数据表
create database chapter22;
use chapter22;
/*==============================================================*/
/* Table: 红包表 */
/*==============================================================*/
create table T_RED_PACKET
(
id int(12) not null auto_increment,
user_id int(12) not null,
amount decimal(16,2) not null,
send_date timestamp not null,
total int(12) not null,
unit_amount decimal(12) not null,
stock int(12) not null,
version int(12) default 0 not null,
note varchar(256) null,
primary key clustered (id)
);
/*==============================================================*/
/* Table: 用户抢红包表 */
/*==============================================================*/
create table T_USER_RED_PACKET
(
id int(12) not null auto_increment,
red_packet_id int(12) not null,
user_id int(12) not null,
amount decimal(16,2) not null,
grab_time timestamp not null,
note varchar(256) null,
primary key clustered (id)
);
/**
* 插入一个20万元金额,100个小红包,每个200元的红包数据
*/
insert into T_RED_PACKET(user_id, amount, send_date, total, unit_amount, stock, note)
values(1, 200000.00, now(), 20000, 200.00,100,'20万元金额,100个小红包,每个200元');
4、两个pojo类
1)、红包类
package com.ssm.chapter22.pojo;
import java.io.Serializable;
import java.sql.Timestamp;
//实现Serializable接口,这样便可序列化对象
//红包表
public class RedPacket implements Serializable{
private static final long serialVersionUID = 1L;
private Long 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;//备注
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Double getAmount() {
return amount;
}
public void setAmount(Double amount) {
this.amount = amount;
}
public Timestamp getSendDate() {
return sendDate;
}
public void setSendDate(Timestamp sendDate) {
this.sendDate = sendDate;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public Double getUnitAmount() {
return unitAmount;
}
public void setUnitAmount(Double unitAmount) {
this.unitAmount = unitAmount;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
2)、抢红包用户类(抢红包信息)
package com.ssm.chapter22.pojo;
import java.io.Serializable;
import java.sql.Timestamp;
//实现Serializable接口,这样便可序列化对象
//抢红包信息表
public class UserRedPacket implements Serializable{
private static final long serialVersionUID = 1L;
private Long id;//编号
private Long redPacketId;//红包编号
private Long userId;//抢红包用户编号
private Double amount;//抢红包金额
private Timestamp grabTime;//抢红包时间
private String note;//备注
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getRedPacketId() {
return redPacketId;
}
public void setRedPacketId(Long redPacketId) {
this.redPacketId = redPacketId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Double getAmount() {
return amount;
}
public void setAmount(Double amount) {
this.amount = amount;
}
public Timestamp getGrabTime() {
return grabTime;
}
public void setGrabTime(Timestamp grabTime) {
this.grabTime = grabTime;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
5、DAO层
1)红包Dao(RedPacketDao.java)
pckage com.ssm.chapter22.dao;
import org.springframework.stereotype.Repository;
import com.ssm.chapter22.pojo.RedPacket;
@Repository
public interface RedPacketDao {
/**
* 获取红包信息.
* @param id --红包id
* @return 红包具体信息
*/
public RedPacket getRedPacket(Long id);
/**
* 扣减抢红包数.
* @param id -- 红包id
* @return 更新记录条数
*/
public int decreaseRedPacket(Long id);
}
2)、抢红包Dao(UserRedPacket.java)
package com.ssm.chapter22.dao;
import org.springframework.stereotype.Repository;
import com.ssm.chapter22.pojo.UserRedPacket;
@Repository
public interface UserRedPacketDao {
/**
* 插入抢红包信息.
* @param userRedPacket ——抢红包信息
* @return 影响记录数.
*/
public int grapRedPacket(UserRedPacket userRedPacket);
}
6、映射文件
1)、RedPacket.xml
<?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.ssm.chapter22.dao.RedPacketDao">
<!-- 查询红包具体信息 -->
<select id="getRedPacket" parameterType="long"
resultType="com.ssm.chapter22.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>
2)、UserRedPacket.xml
<?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.ssm.chapter22.dao.UserRedPacketDao">
<!-- 插入抢红包信息 -->
<insert id="grapRedPacket" useGeneratedKeys="true"
keyProperty="id" parameterType="com.ssm.chapter22.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>
注意:
1)、这里的隔离级别采用”读/写提交“,之所以不采用更高的隔离级别,主要是提高数据库的并发能力;
2)、传播行为采用“Propagation.REQUIRED”,这样调用这个方法的时候,如果没有事务会创建事务,如果有事务沿用当前事务。
7、Service层
1)、RedPacketService.java接口
package com.ssm.chapter22.service;
import com.ssm.chapter22.pojo.RedPacket;
public interface RedPacketService {
/**
* 获取红包
* @param id ——编号
* @return 红包信息
*/
public RedPacket getRedPacket(Long id);
/**
* 扣减红包
* @param id——编号
* @return 影响条数.
*/
public int decreaseRedPacket(Long id);
}
2)、UserRedP acketService.java接口
package com.ssm.chapter22.service;
public interface UserRedPacketService {
/**
* 保存抢红包信息.
* @param redPacketId 红包编号
* @param userId 抢红包用户编号
* @return 影响记录数.
*/
public int grapRedPacket(Long redPacketId, Long userId);
}
3)、Red PacketServiceImpl.java(接口的实现类)
package com.ssm.chapter22.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.ssm.chapter22.dao.RedPacketDao;
import com.ssm.chapter22.pojo.RedPacket;
import com.ssm.chapter22.service.RedPacketService;
@Service
public class RedPacketServiceImple implements RedPacketService {
@Autowired
private RedPacketDao redPacketDao = null;
@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);
}
}
4)、UserRedPacketServiceI mpl.java(接口的实现类)
package com.ssm.chapter22.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.ssm.chapter22.dao.RedPacketDao;
import com.ssm.chapter22.dao.UserRedPacketDao;
import com.ssm.chapter22.pojo.RedPacket;
import com.ssm.chapter22.pojo.UserRedPacket;
import com.ssm.chapter22.service.UserRedPacketService;
@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {
@Autowired
private UserRedPacketDao userRedPacketDao = null;
@Autowired
private RedPacketDao redPacketDao = null;
// 失败
private static final int FAILED = 0;
//grapRedPacket方法的逻辑是:首先获取红包的信息,如果发现红包库存大于0,
//则说明还有红包可以抢,抢夺红包并生成抢红包的信息将其保存到数据库中。
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId) {
// 获取红包信息
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
// 当前小红包库存大于0
if (redPacket.getStock() > 0) {
redPacketDao.decreaseRedPacket(redPacketId);
// 生成抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包 " + redPacketId);
// 插入抢红包信息
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}
// 失败返回
return FAILED;
}
}
注意:
1)、这里的隔离级别采用”读/写提交“,之所以不采用更高的隔离级别,主要是提高数据库的并发能力;
2)、传播行为采用“Propagation.REQUIRED”,这样调用这个方法的时候,如果没有事务会创建事务,如果有事务沿用当前事务。
8、使用全注解方式开发SSM开发环境
1)、首先来配置WebAppInitializer
package com.ssm.chapter22.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 = "e:/mvc/uploads";
// 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));
}
}
2)、RootConfig.java
package com.ssm.chapter22.config;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan.Filter;
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;
}
Properties props = new Properties();
props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
props.setProperty("url", "jdbc:mysql://localhost:3306/chapter22");
props.setProperty("username", "root");
props.setProperty("password", "123456");
props.setProperty("maxActive", "200");
props.setProperty("maxIdle", "20");
props.setProperty("maxWait", "30000");
try {
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;
}
}
3)、Webconfig.java
package com.ssm.chapter22.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@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<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(mediaType);
//加入转换器的支持类型
jsonConverter.setSupportedMediaTypes(mediaTypes);
//往适配器加入json转换器
rmhd.getMessageConverters().add(jsonConverter);
return rmhd;
}
}
9、Mybatis配置文件
<?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="com/ssm/chapter22/mapper/UserRedPacket.xml"/>
<mapper resource="com/ssm/chapter22/mapper/RedPacket.xml"/>
</mappers>
</configuration>
10、Con troller控制层
package com.ssm.chapter22.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.ssm.chapter22.service.UserRedPacketService;
@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
@Autowired
private UserRedPacketService userRedPacketService = null;
@RequestMapping(value = "/grapRedPacket")
@ResponseBody
public Map<String, Object> grapRedPacket(Long redPacketId,Long userId) {
// 抢红包
System.out.println(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;
}
}
11、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>Insert title here</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$(document).ready(function () {
//模拟200个异步请求,进行并发,总共有100个小红包
var max = 200;
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>
12、测试步骤
启动服务器,在浏览器输入:http://localhost:8080/Chapter22/grap.jsp
13、结果
出现超发现象,在6秒内全抢完。