最近在看Java多线程相关的知识,看到了超发的现象,所以搭了个环境跑了下,顺便复习下SSM框架的搭建。
数据库:mysql
IDE:Eclipse
项目管理工具:maven
相关技术框架:spring,springMVC,mybatis,redis
这里为了研究超发现象,并没有加入悲观锁,乐观锁和Lua语言+redis来实现
这里给出项目结构图:
一、下面先看下表的构建,表就两张,一张是 红包表t_red_packet,另外一张是用户抢红包的相关信息表t_user_red_packet
1.t_red_packet
2.t_user_red_packet
二、由于这里我是使用Maven来管理项目的,所以我下面给出pom.xml,相关包的依赖(里面redis相关包,是下一次才用到的,这次可以不用导入)
<dependencies>
<!-- 单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- Redis-Java -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- spring-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<!-- 2.数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- DAO: MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 3.Servlet web -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- 返回json格式,使用时需要的转换包 如responsebody注解 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- 4.Spring -->
<!-- 1)Spring核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!-- 2)Spring DAO层 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!-- 3)Spring web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker-gae</artifactId>
<version>2.3.28</version>
</dependency>
<!-- 4)Spring test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
</dependencies>
三.基础的配置文件,这里我还是选择xml配置的方式,没有用Java注解的方式
1.spring-dao.xml 这里主要配置Dao层相关
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置整合mybatis过程 -->
<!-- 1.配置数据库相关参数properties的属性:${url} -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 2.数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false" />
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="3000" />
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2" />
</bean>
<!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
<!-- 扫描entity包 使用别名 -->
<property name="typeAliasesPackage" value="com.lemonner.cgpt.entity" />
<!-- 扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath:com/mapper/*.xml" />
</bean>
<!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="com.dao" />
</bean>
</beans>
2.spring-service.xml 服务层基本配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="com.*"></context:component-scan>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置基于注解的声明式事务
默认使用注解来管理事务行为-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
3.spring-web.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置SpringMVC -->
<!-- 1.开启SpringMVC注解模式 -->
<mvc:annotation-driven/>
<!-- 2.配置jsp 显示ViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!--3:扫描web相关的controller-->
<context:component-scan base-package="com.controller"/>
</beans>
4.jdbc.propertis(url, username,password 改成自己本地mysql的)
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/testRedis?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=1223360222
5.log4j.properties
log4j.rootLogger=INFO,Console,File
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n
log4j.appender.File = org.apache.log4j.RollingFileAppender
log4j.appender.File.File = logs/ssm.log
log4j.appender.File.MaxFileSize = 10MB
log4j.appender.File.Threshold = ALL
log4j.appender.File.layout = org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n
6.web.xml配置,web.xml是项目的入口,所以我们在web.xml再把前面的spring-*.xml配置放在web.xml里面
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Archetype Created Web Application</display-name>
<!-- 配置 ContextLoaderListener 用以初始化Spring IoC容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 加载spring系列配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
<!-- 使得dispatcher在服务器启动的时候就初始化 -->
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!-- 此处可以可以配置成*.do,对应struts的后缀习惯 -->
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
四、搭建MVC层
1.com.pojo(实体类)
里面主要由两个实体类,一个是RedPacket 红包类, 一个是UserRedPacket 用户抢红包信息类,下面给出代码:
a).RedPacket
public class RedPacket implements Serializable{
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;
...
...
getter,setter
}
b).UserRedPacket类
public class UserRedPacket implements Serializable{
private Long id;
private Long redPacketId;
private Long userId;
private Double amount;
private Timestamp grabTime;
private String note;
...
getter,setter
}
2.com.dao Dao 层接口
a).RedPacketDao
import org.springframework.stereotype.Repository;
import com.pojo.RedPacket;
@Repository
public interface RedPacketDao {
/*
* 获取红包信息
* @param id 红包id
* @return 红包具体信息
*/
public RedPacket getRedPacket(Long id);
/*
* 扣减抢红包数
* @param id 红包id
* @return 更新记录条数
*/
public int decreaseRedPacket(Long id);
}
b).UserRedPacketDao
import org.springframework.stereotype.Repository;
import com.pojo.UserRedPacket;
@Repository
public interface UserRedPacketDao {
/*
* 插入抢红包信息
* @param userRedPacket 抢红包信息
* @return 影响记录数
*/
public int grapRedPacket(UserRedPacket userRedPacket);
}
3.com.mapper Dao层sql的书写
a).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.dao.RedPacketDao">
<!-- 查询红包具体信息 -->
<select id="getRedPacket" parameterType="long"
resultType="com.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>
b).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.dao.UserRedPacketDao">
<!-- 插入抢红包信息 -->
<insert id="grapRedPacket" useGeneratedKeys="true"
keyProperty="id" parameterType="com.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>
4.com.service 服务层接口
a).RedPacketService
import com.pojo.RedPacket;
public interface RedPacketService {
/*
* 获取红包
* @param id 红包id
* @return 红包具体信息
*/
public RedPacket getRedPacket(Long id);
/*
* 扣减红包
* @param id 红包id
* @return 更新记录条数
*/
public int decreaseRedPacket(Long id);
}
b).UserRedPacketService
public interface UserRedPacketService {
/*
* 保存抢红包信息
* @param redPacketId 红包编号
* @param userId 抢红包用户编号
* @return 影响记录数
*/
public int grapRedPacket(Long redPacketId, Long userId);
}
5.com.service.impl 服务层的实现类
a).RedPacketServiceImpl
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.dao.RedPacketDao;
import com.pojo.RedPacket;
import com.service.RedPacketService;
@Service
public class RedPacketServiceImpl implements RedPacketService {
@Autowired
private RedPacketDao redPacketDao = null;
@Override
@Transactional(isolation=Isolation.READ_COMMITTED, propagation=Propagation.REQUIRED)
public RedPacket getRedPacket(Long id) {
// TODO Auto-generated method stub
return redPacketDao.getRedPacket(id);
}
@Override
@Transactional(isolation=Isolation.READ_COMMITTED, propagation=Propagation.REQUIRED)
public int decreaseRedPacket(Long id) {
// TODO Auto-generated method stub
return redPacketDao.decreaseRedPacket(id);
}
}
b).UserRedPacketServiceImpl
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.dao.RedPacketDao;
import com.dao.UserRedPacketDao;
import com.pojo.RedPacket;
import com.pojo.UserRedPacket;
import com.service.UserRedPacketService;
@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {
@Autowired
private UserRedPacketDao userRedPacketDao = null;
@Autowired
private RedPacketDao redPacketDao = null;
//失败
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);
//当前小红包库存大于0
if(redPacket.getStock() > 0) {
redPacketDao.decreaseRedPacket(redPacketId);
//生成抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包 "+ redPacketId);
//插入抢红包信息
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}
return FAILED;
}
}
6.com.controller 控制层的书写 这里就一个单一的UserRedPacketController控制类
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.service.UserRedPacketService;
@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
@Autowired
private UserRedPacketService userRedPacketService = null;
@RequestMapping("/grapRedPacket")
@ResponseBody
public Map<String, Object> grapRedPacket(Long redPacketId, Long userId){
//抢红包
int result = userRedPacketService.grapRedPacket(redPacketId, userId);
Map<String,Object> res = new HashMap<String,Object>();
boolean flag = result > 0;
res.put("success", flag);
res.put("message", flag?"抢红包成功":"抢红包失败");
return res;
}
}
到这里就差不多完成了,测试的话我这里是用一个JavaScript来模拟两万人同时抢红包的场景,如果要用JavaScript来测试的话,不要用tomcat内置的浏览器 , Google 和360急速浏览器都会发生请求丢失,这里可以用火狐来测试,fireFox
下面给出测试的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: "${pageContext.request.contextPath }/userRedPacket/grapRedPacket.do?redPacketId=6&userId=" + i,
//成功后的方法
success: function (result) {
}
});
}
});
</script>
</head>
<body>
我这里的测试结果
这里可以看出他超发了1个红包
再看下的他的性能:
差不多用了1分钟吧,这里速度也和电脑的自身性能相关的。
这种超发现象主要是多线程下数据不一致造成的,这里的主要解决办法是用乐观锁或悲观锁或者利用Lua的原子性配合Redis来解决的