SSM+Redis高并发抢红包之-超发现象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/toward_south/article/details/89736457

最近在看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来解决的

关于如何在Java代码上实现,是否能解决超发现象,性能怎么样可以看我下一篇博客SSM+Redis高并发抢红包之-悲观锁

SSM+Redis高并发抢红包之-乐观锁

SSM+Redis高并发抢红包之-Lua+Redis

如果没看见,那就是还没写了

猜你喜欢

转载自blog.csdn.net/toward_south/article/details/89736457
今日推荐