RabitMQ系列之 Spring RabbitMQ流量削锋实现案例之抢红包

版权声明:如需转载,请注明出处。 https://blog.csdn.net/caiqing116/article/details/84640688

一、案例说明:电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验,我们都知道,秒杀类,抢购类应用都有一个共同点,即瞬时请求巨大。本文中用一个多用户抢红包来类比秒杀抢购类的应用。普通的抢红包实现如下图:
在这里插入图片描述
当开始进行抢红包时,系统将会瞬时有大量的请求(高并发),接口业务逻辑处理导致应用占据服务器资源飙升,为了避免宕机,我们可以在将此高并发量的请求拒之于系统的上层,即将抢红包请求先压入消息队列, 而不直接涌入我们的系统后台业务处理接口。对此我们的优化方案如下图:
在这里插入图片描述
我们将使用RabbitMQ 在请求与接口之间充当限流缓压(流量削锋)的角色,对RabbitMQ需要更高要求的配置,及高并发配置,配置效果图如下,具体配置详见下文:
在这里插入图片描述
对每个listener在初始化的时候设置的并发消费者的个数为5,每个 consumer 可以预监听消费拉取的消息数量为 5 个,如果同一时间处理不完,会将其缓存在 mq 的客户端等待处理!

案例具体实现git地址:https://github.com/gitcaiqing/RabbitMQ-RedPack

创建数据库和数据表

CREATE DATABASE db_redpack CHARACTER SET UTF8;

创建用户发红包记录表

CREATE TABLE tb_redpack(
	id int not null AUTO_INCREMENT,
	userId varchar(32) not null comment '发红包用户',
	amount decimal(10,2) not null comment '红包金额',
	unitAmount decimal(10,2) not null comment '单个红包金额',
	total int not null comment '红包个数',
	sendDate datetime not null comment '发红包时间',
	version int default 0 not null comment '版本控制,扩展乐观锁使用',
	primary key(id)
);

创建用户抢红包记录

CREATE TABLE tb_user_redpack(
	id int not null AUTO_INCREMENT,
	userId varchar(32) not null comment '抢红包用户',
	redpackId int not null comment '发红包记录id',
	amount decimal(10,2) not null comment '抢的红包金额',
	grabDate datetime not null comment '抢红包时间',
	version int default 0 not null comment '版本控制,扩展乐观锁使用',
	primary key(id)
);

插入测试数据,这里我为了降低处理红包金额的复杂度,故意整成固定的金额,我们将发1000个红包,每个红包10.00元。

INSERT INTO tb_redpack(userId,amount,unitAmount,total,sendDate)
VALUES("9999",100000.00,10.00,10000,now());

二、搭建项目
1.项目结构
在这里插入图片描述

2.Maven引入需要的jar包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.sc</groupId>
  <artifactId>RabbitMQ-RedPack</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.1.4.RELEASE</spring.version>
	<jackson.version>2.5.0</jackson.version>
	<shiro.version>1.2.3</shiro.version>
  </properties>
  
  <dependencies>
  		<!--rabbitmq包 -->
  		<dependency>
			<groupId>com.rabbitmq</groupId>
			<artifactId>amqp-client</artifactId>
			<version>3.4.1</version>
		</dependency>
		<dependency>
       		<groupId>org.springframework.amqp</groupId>
       		<artifactId>spring-rabbit</artifactId>
       		<version>1.4.0.RELEASE</version>
    	</dependency> 
    	
   		<!-- spring核心包 -->  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-core</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-web</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-oxm</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-tx</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-jdbc</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-webmvc</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-aop</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-context-support</artifactId>  
            <version>${spring.version}</version>  
        </dependency>  
  
        <dependency>  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-test</artifactId>  
            <version>${spring.version}</version>  
        </dependency> 
	
		<!-- mybatis 包 -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.2.8</version>
		</dependency>
	
		<!--mybatis spring 插件 -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>1.2.2</version>
		</dependency>
		
		<!-- mybatis分页插件 -->
	    <dependency>
	        <groupId>com.github.miemiedev</groupId>
	        <artifactId>mybatis-paginator</artifactId>
	        <version>1.2.17</version>
	    </dependency>
	
		<!-- mysql连接 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.34</version>
		</dependency>
	
		<!-- 数据源 -->
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.3</version>
		</dependency>
	
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.8.4</version>
		</dependency>
	
		<!-- log4j -->
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.17</version>
		</dependency>
		
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.7</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.7</version>
		</dependency>
	
		<!-- servlet -->
		<dependency>
		    <groupId>javax.servlet</groupId>
		    <artifactId>javax.servlet-api</artifactId>
		    <version>3.0.1</version>
		    <scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
	
		<!-- json -->
		<dependency>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
			<version>1.9.13</version>
		</dependency>
	
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>${jackson.version}</version>
		</dependency>
	
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
			<version>${jackson.version}</version>
		</dependency>
	
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>${jackson.version}</version>
		</dependency>
		
		
		<!-- 常用工具包 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.3.2</version>
		</dependency>
	
  </dependencies>
  <build>
    <plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.2</version>
			<configuration>
				<source>1.7</source>
				<target>1.7</target>
			</configuration>
		</plugin>
	</plugins>
  </build>
</project>

3.web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 
	xmlns="http://java.sun.com/xml/ns/javaee" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
	http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
   <display-name>RabbitMQ-RedPack</display-name>	
   
   <!-- 配置文件 -->  
   <context-param> 
		<param-name>contextConfigLocation</param-name> 
 		<param-value> classpath*:spring/*.xml,classpath*:servlet/*.xml</param-value> 
 	</context-param> 
 	
 	<!-- Spring监听器 -->  
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
    <!-- 编码过滤器 -->  
	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>utf-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	
	<!-- Spring MVC servlet -->  
	<servlet>
		<servlet-name>springMvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
				classpath*:servlet/*.xml
			</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>springMvc</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
  
	<session-config>
		<session-timeout>30</session-timeout>
	</session-config>
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
</web-app>

4.JDBC和RabbitMQ连接配置
jdbc.properties

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/db_redpack?useUnicode=true&amp;characterEncoding=UTF-8&allowMultiQueries=true
jdbc.username=root
jdbc.password=123456
jdbc.initialSize=2
jdbc.maxActive=5
jdbc.maxIdle=5
jdbc.minIdle=1
jdbc.validationQuery=select 1

config.properties

#RabbitMQ 连接配置
rmq.host=127.0.0.1
rmq.port=5672
rmq.user=guest
rmq.password=guest
rmq.channelCacheSize=25
rmq.virtual=/

5.Spring,Mybatis,数据源配置
spring/applicationContext.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:task="http://www.springframework.org/schema/task" 
	   xmlns:cache="http://www.springframework.org/schema/cache" 
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd 
		http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd 
		http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

	<!--引入配置属性文件 -->
	<context:property-placeholder location="classpath*:config/*.properties" />
	<context:component-scan base-package="com.sc">
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
	</context:component-scan>

</beans>

spring/dataAccessContext.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"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
	<description>数据库、事务配置</description>
	<!-- 数据源定义,使用Apache DBCP 连接池 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="initialSize" value="${jdbc.initialSize}" />
		<property name="maxActive" value="${jdbc.maxActive}" />
		<property name="maxIdle" value="${jdbc.maxIdle}" />
		<property name="minIdle" value="${jdbc.minIdle}" />
		<property name="validationQuery" value="${jdbc.validationQuery}" />
	</bean>
	<!-- 使用annotation定义事务,在每个需要事务的类上,请加上@Transactional注解 -->
	<tx:annotation-driven transaction-manager="transactionManager"  />
	<!--事务配置 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
</beans>

spring/mybatis.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:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
	
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="mapperLocations">
			 <list>
                <value>classpath:sql/*.xml</value>
            </list>
		</property>
	</bean>

	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.sc.mapper" />
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	</bean>
	
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>
</beans>

6.SpringMVC配置 servlet/spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-4.1.xsd 
		http://www.springframework.org/schema/mvc 
		http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">
		
	<!-- 自动扫描,只扫描Controller -->
    <!-- <context:component-scan base-package="com.sc"></context:component-scan>	 -->
    <!-- 自动扫描,只扫描Controller -->
    <context:component-scan base-package="com.sc">
       <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
       <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
    </context:component-scan>	
    
    <!-- 启用spring mvc 注解 -->
    <mvc:annotation-driven>
		<mvc:message-converters register-defaults="true">
			<!-- 将StringHttpMessageConverter的默认编码设为UTF-8 -->
			<bean class="org.springframework.http.converter.StringHttpMessageConverter">
		    	<constructor-arg value="UTF-8" />
			</bean>
			<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>  
			<!-- 将Jackson2HttpMessageConverter的默认格式化输出设为true -->
			<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="prettyPrint" value="true"/>
            </bean>			
  		</mvc:message-converters>
	</mvc:annotation-driven>
	
	<!-- 静态资源 -->
	<mvc:resources mapping="/static/**" location="/static/" />
	<mvc:resources mapping="/**" location="/" />
    
    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    	<property name="prefix" value="/WEB-INF/views"></property>
    	<property name="suffix" value=".jsp"></property>
    	<property name="order" value="1"></property>
    </bean>
    
    
</beans>

7.Spring RabbitMQ配置 spring/spring-rabbitmq.xml

 <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:rabbit="http://www.springframework.org/schema/rabbit"
	xsi:schemaLocation="http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.4.xsd
	http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
	<description>Spring RabbitMQ 路由模式(Routing)配置</description>
	
	<!--引入配置属性文件 -->
	<context:property-placeholder location="classpath:config/*.properties" />
	
 	<!-- 配置RabbitMQ连接 -->
 	<!-- channel-cache-size,channel的缓存数量,默认值为25 -->
 	<!-- cache-mode,缓存连接模式,默认值为CHANNEL(单个connection连接,连接之后关闭,自动销毁) -->
 	<rabbit:connection-factory id="connectionFactory" host="${rmq.host}" port="${rmq.port}"
 		username="${rmq.user}" password="${rmq.password}" virtual-host="${rmq.virtual}" channel-cache-size="${rmq.channelCacheSize}" cache-mode="CHANNEL"/>
 	<rabbit:admin connection-factory="connectionFactory"/>
 
 	<!--
 		定义消息队列
 		durable:是否持久化,如果想在RabbitMQ退出或崩溃的时候,不失去queue和消息,需要同时标志队列(queue)
 		和交换机(exchange)持久化,即rabbit:queue标签和rabbit:direct-exchange中的durable=true,而消息(message)
 		默认是持久化的,可以看类org.springframework.amqp.core.MessageProperties中的属性
 		public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
 		auto_delete: 当所有消费客户端连接断开后,是否自动删除队列 
 		exclusive: 仅创建者可以使用的私有队列,断开后自动删除;
 		
 	-->
 	<rabbit:queue id="redpackqueue" name="SpringRabbit-RedPack-Queue" durable="true" auto-delete="false" exclusive="false">
 		<rabbit:queue-arguments>
 			<entry key="x-expires">
           		<value type="java.lang.Integer">10000</value>
        	</entry>
 		</rabbit:queue-arguments>
 	</rabbit:queue>
 	
 	<!--
 		绑定队列
 		rabbitmq的exchangeType常用的三种模式:direct,fanout,topic三种,此处为direct模式,即rabbit:direct-exchange标签,
 		Direct交换器很简单,如果是Direct类型,就会将消息中的RoutingKey与该Exchange关联的所有Binding中的BindingKey进行比较,如果相等,
 		则发送到该Binding对应的Queue中。有一个需要注意的地方:如果找不到指定的exchange,就会报错。
 		但routing key找不到的话,不会报错,这条消息会直接丢失,所以此处要小心,
 	    auto-delete:自动删除,如果为Yes,则该交换机所有队列queue删除后,自动删除交换机,默认为false 
 	-->
 	<rabbit:direct-exchange name="SpringRabbit-RedPack-Exchange" durable="true" auto-declare="true" auto-delete="false">
 		<rabbit:bindings>
 			<rabbit:binding queue="redpackqueue" key="info"></rabbit:binding>
 		</rabbit:bindings>
 	</rabbit:direct-exchange>
 	
 	<!-- spring template声明 -->
 	<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" exchange="SpringRabbit-RedPack-Exchange"></rabbit:template>

	<!-- 消费者  -->
 	<bean id="consumer" class="com.sc.consumer.RedPackConsumer"/>
	<!-- 队列监听-->
	<!-- acknowledge:auto 自动确认(默认), manual手动确认  -->
	<!-- concurrency:并发数量 ,设置的是对每个listener在初始化的时候设置的并发消费者的个数-->
	<!-- prefetch 是每次从一次性从broker里面取的待消费的消息的个数 -->
 	<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" concurrency="5" prefetch="5">
 		<rabbit:listener queues="redpackqueue" ref="consumer" />
 	</rabbit:listener-container>
 	
</beans>

8.根据表结构生成对应的实体类和SQL映射文件
参考文章 https://mp.csdn.net/mdeditor/84586914 自动生成文件。也可在我的git中下载本项目源码。地址在文章开头已给出,下面我们贴出具体实现,参考实现时给你们可以偷个懒,哈哈。
src/main/java/com/sc/entity/Redpack.java

package com.sc.entity;

import java.math.BigDecimal;
import java.util.Date;

public class Redpack {
    /**
     * 
     */
    private Integer id;

    /**
     * 发红包用户
     */
    private String userid;

    /**
     * 红包金额
     */
    private BigDecimal amount;

    /**
     * 单个红包金额
     */
    private BigDecimal unitamount;

    /**
     * 红包个数
     */
    private Integer total;

    /**
     * 红包剩余个数
     */
    private Integer remain;

    /**
     * 发红包时间
     */
    private Date senddate;

    /**
     * 版本控制,扩展乐观锁使用
     */
    private Integer version;

    /**
     * 
     * @return id 
     */
    public Integer getId() {
        return id;
    }

    /**
     * 
     * @param id 
     */
    public void setId(Integer id) {
        this.id = id;
    }

    /**
     * 发红包用户
     * @return userId 发红包用户
     */
    public String getUserid() {
        return userid;
    }

    /**
     * 发红包用户
     * @param userid 发红包用户
     */
    public void setUserid(String userid) {
        this.userid = userid == null ? null : userid.trim();
    }

    /**
     * 红包金额
     * @return amount 红包金额
     */
    public BigDecimal getAmount() {
        return amount;
    }

    /**
     * 红包金额
     * @param amount 红包金额
     */
    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    /**
     * 单个红包金额
     * @return unitAmount 单个红包金额
     */
    public BigDecimal getUnitamount() {
        return unitamount;
    }

    /**
     * 单个红包金额
     * @param unitamount 单个红包金额
     */
    public void setUnitamount(BigDecimal unitamount) {
        this.unitamount = unitamount;
    }

    /**
     * 红包个数
     * @return total 红包个数
     */
    public Integer getTotal() {
        return total;
    }

    /**
     * 红包个数
     * @param total 红包个数
     */
    public void setTotal(Integer total) {
        this.total = total;
    }

    /**
     * 红包剩余个数
     * @return remain 红包剩余个数
     */
    public Integer getRemain() {
        return remain;
    }

    /**
     * 红包剩余个数
     * @param remain 红包剩余个数
     */
    public void setRemain(Integer remain) {
        this.remain = remain;
    }

    /**
     * 发红包时间
     * @return sendDate 发红包时间
     */
    public Date getSenddate() {
        return senddate;
    }

    /**
     * 发红包时间
     * @param senddate 发红包时间
     */
    public void setSenddate(Date senddate) {
        this.senddate = senddate;
    }

    /**
     * 版本控制,扩展乐观锁使用
     * @return version 版本控制,扩展乐观锁使用
     */
    public Integer getVersion() {
        return version;
    }

    /**
     * 版本控制,扩展乐观锁使用
     * @param version 版本控制,扩展乐观锁使用
     */
    public void setVersion(Integer version) {
        this.version = version;
    }
}

src/main/java/com/sc/entity/UserRedpack.java

package com.sc.entity;

import java.math.BigDecimal;
import java.util.Date;

public class UserRedpack {
    /**
     * 
     */
    private Integer id;

    /**
     * 抢红包用户
     */
    private String userid;

    /**
     * 发红包记录id
     */
    private Integer redpackid;

    /**
     * 抢的红包金额
     */
    private BigDecimal amount;

    /**
     * 抢红包时间
     */
    private Date grabdate;

    /**
     * 版本控制,扩展乐观锁使用
     */
    private Integer version;

    /**
     * 
     * @return id 
     */
    public Integer getId() {
        return id;
    }

    /**
     * 
     * @param id 
     */
    public void setId(Integer id) {
        this.id = id;
    }

    /**
     * 抢红包用户
     * @return userId 抢红包用户
     */
    public String getUserid() {
        return userid;
    }

    /**
     * 抢红包用户
     * @param userid 抢红包用户
     */
    public void setUserid(String userid) {
        this.userid = userid == null ? null : userid.trim();
    }

    /**
     * 发红包记录id
     * @return redpackId 发红包记录id
     */
    public Integer getRedpackid() {
        return redpackid;
    }

    /**
     * 发红包记录id
     * @param redpackid 发红包记录id
     */
    public void setRedpackid(Integer redpackid) {
        this.redpackid = redpackid;
    }

    /**
     * 抢的红包金额
     * @return amount 抢的红包金额
     */
    public BigDecimal getAmount() {
        return amount;
    }

    /**
     * 抢的红包金额
     * @param amount 抢的红包金额
     */
    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    /**
     * 抢红包时间
     * @return grabDate 抢红包时间
     */
    public Date getGrabdate() {
        return grabdate;
    }

    /**
     * 抢红包时间
     * @param grabdate 抢红包时间
     */
    public void setGrabdate(Date grabdate) {
        this.grabdate = grabdate;
    }

    /**
     * 版本控制,扩展乐观锁使用
     * @return version 版本控制,扩展乐观锁使用
     */
    public Integer getVersion() {
        return version;
    }

    /**
     * 版本控制,扩展乐观锁使用
     * @param version 版本控制,扩展乐观锁使用
     */
    public void setVersion(Integer version) {
        this.version = version;
    }
}

src/main/java/com/sc/mapper/ResultModel.java

package com.sc.entity;

public class ResultModel<T> {

	public int resultCode = 0;//0成功1失败
	public T data;
	public String msg;
	public int getResultCode() {
		return resultCode;
	}
	public ResultModel(int resultCode, T data, String msg) {
		super();
		this.resultCode = resultCode;
		this.data = data;
		this.msg = msg;
	}
	public void setResultCode(int resultCode) {
		this.resultCode = resultCode;
	}
	public T getData() {
		return data;
	}
	public void setData(T data) {
		this.data = data;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}
	
}

src/main/java/com/sc/mapper/RedpackMapper.java

import com.sc.entity.Redpack;

public interface RedpackMapper {
	
    int deleteByPrimaryKey(Integer id);
    
    int insert(Redpack record);
    
    int insertSelective(Redpack record);
    
    Redpack selectByPrimaryKey(Integer id);
    
    int updateByPrimaryKeySelective(Redpack record);
    
    int updateByPrimaryKey(Redpack record);
    
    /**
     * 查询红包剩余个数
     * @param id
     * @return
     */
    int selectRemainByPrimaryKey(Integer id);

    /**
     * 扣减红包剩余个数
     * @param id
     * @return
     */
	int updateRemainRedPack(Integer id);
}

src/main/java/com/sc/mapper/UserRedpackMapper.java

package com.sc.mapper;

import com.sc.entity.UserRedpack;

public interface UserRedpackMapper {
    int deleteByPrimaryKey(Integer id);
    
    int insert(UserRedpack record);
    
    int insertSelective(UserRedpack record);
    
    UserRedpack selectByPrimaryKey(Integer id);
    
    int updateByPrimaryKeySelective(UserRedpack record);
    
    int updateByPrimaryKey(UserRedpack record);
}

src/main/resources/sql/UserRedpackMapper.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.sc.mapper.UserRedpackMapper" >
  <resultMap id="BaseResultMap" type="com.sc.entity.UserRedpack" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="userId" property="userid" jdbcType="VARCHAR" />
    <result column="redpackId" property="redpackid" jdbcType="INTEGER" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="grabDate" property="grabdate" jdbcType="TIMESTAMP" />
    <result column="version" property="version" jdbcType="INTEGER" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, userId, redpackId, amount, grabDate, version
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from tb_user_redpack
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from tb_user_redpack
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.sc.entity.UserRedpack" >
    insert into tb_user_redpack (id, userId, redpackId, 
      amount, grabDate, version
      )
    values (#{id,jdbcType=INTEGER}, #{userid,jdbcType=VARCHAR}, #{redpackid,jdbcType=INTEGER}, 
      #{amount,jdbcType=DECIMAL}, #{grabdate,jdbcType=TIMESTAMP}, #{version,jdbcType=INTEGER}
      )
  </insert>
  <insert id="insertSelective" parameterType="com.sc.entity.UserRedpack" >
    insert into tb_user_redpack
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userid != null" >
        userId,
      </if>
      <if test="redpackid != null" >
        redpackId,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="grabdate != null" >
        grabDate,
      </if>
      <if test="version != null" >
        version,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userid != null" >
        #{userid,jdbcType=VARCHAR},
      </if>
      <if test="redpackid != null" >
        #{redpackid,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="grabdate != null" >
        #{grabdate,jdbcType=TIMESTAMP},
      </if>
      <if test="version != null" >
        #{version,jdbcType=INTEGER},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.sc.entity.UserRedpack" >
    update tb_user_redpack
    <set >
      <if test="userid != null" >
        userId = #{userid,jdbcType=VARCHAR},
      </if>
      <if test="redpackid != null" >
        redpackId = #{redpackid,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="grabdate != null" >
        grabDate = #{grabdate,jdbcType=TIMESTAMP},
      </if>
      <if test="version != null" >
        version = #{version,jdbcType=INTEGER},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.sc.entity.UserRedpack" >
    update tb_user_redpack
    set userId = #{userid,jdbcType=VARCHAR},
      redpackId = #{redpackid,jdbcType=INTEGER},
      amount = #{amount,jdbcType=DECIMAL},
      grabDate = #{grabdate,jdbcType=TIMESTAMP},
      version = #{version,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>

src/main/resources/sql/RedpackMapper.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.sc.mapper.RedpackMapper" >
  <resultMap id="BaseResultMap" type="com.sc.entity.Redpack" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="userId" property="userid" jdbcType="VARCHAR" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="unitAmount" property="unitamount" jdbcType="DECIMAL" />
    <result column="total" property="total" jdbcType="INTEGER" />
    <result column="remain" property="remain" jdbcType="INTEGER" />
    <result column="sendDate" property="senddate" jdbcType="TIMESTAMP" />
    <result column="version" property="version" jdbcType="INTEGER" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, userId, amount, unitAmount, total, remain, sendDate, version
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from tb_redpack
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from tb_redpack
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.sc.entity.Redpack" >
    insert into tb_redpack (id, userId, amount, 
      unitAmount, total, remain, 
      sendDate, version)
    values (#{id,jdbcType=INTEGER}, #{userid,jdbcType=VARCHAR}, #{amount,jdbcType=DECIMAL}, 
      #{unitamount,jdbcType=DECIMAL}, #{total,jdbcType=INTEGER}, #{remain,jdbcType=INTEGER}, 
      #{senddate,jdbcType=TIMESTAMP}, #{version,jdbcType=INTEGER})
  </insert>
  <insert id="insertSelective" parameterType="com.sc.entity.Redpack" >
    insert into tb_redpack
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userid != null" >
        userId,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="unitamount != null" >
        unitAmount,
      </if>
      <if test="total != null" >
        total,
      </if>
      <if test="remain != null" >
        remain,
      </if>
      <if test="senddate != null" >
        sendDate,
      </if>
      <if test="version != null" >
        version,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userid != null" >
        #{userid,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="unitamount != null" >
        #{unitamount,jdbcType=DECIMAL},
      </if>
      <if test="total != null" >
        #{total,jdbcType=INTEGER},
      </if>
      <if test="remain != null" >
        #{remain,jdbcType=INTEGER},
      </if>
      <if test="senddate != null" >
        #{senddate,jdbcType=TIMESTAMP},
      </if>
      <if test="version != null" >
        #{version,jdbcType=INTEGER},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.sc.entity.Redpack" >
    update tb_redpack
    <set >
      <if test="userid != null" >
        userId = #{userid,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="unitamount != null" >
        unitAmount = #{unitamount,jdbcType=DECIMAL},
      </if>
      <if test="total != null" >
        total = #{total,jdbcType=INTEGER},
      </if>
      <if test="remain != null" >
        remain = #{remain,jdbcType=INTEGER},
      </if>
      <if test="senddate != null" >
        sendDate = #{senddate,jdbcType=TIMESTAMP},
      </if>
      <if test="version != null" >
        version = #{version,jdbcType=INTEGER},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.sc.entity.Redpack" >
    update tb_redpack
    set userId = #{userid,jdbcType=VARCHAR},
      amount = #{amount,jdbcType=DECIMAL},
      unitAmount = #{unitamount,jdbcType=DECIMAL},
      total = #{total,jdbcType=INTEGER},
      remain = #{remain,jdbcType=INTEGER},
      sendDate = #{senddate,jdbcType=TIMESTAMP},
      version = #{version,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>
  <!-- 查询红包剩余个数 -->
  <select id="selectRemainByPrimaryKey" resultType="java.lang.Integer" parameterType="java.lang.Integer">
    select remain
    from tb_redpack
    where id = #{id,jdbcType=INTEGER}
  </select>
  <!-- 扣减红包剩余个数 -->
  <update id="updateRemainRedPack" parameterType="java.lang.Integer">
  	update tb_redpack set remain = remain - 1
  	where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>

9.定义发送队列消息、红包、用户红包、抢红包业务Service接口
src/main/java/com/sc/service/CommonMqService.java

package com.sc.service;

public interface CommonMqService {

	//插入用户抢红包信息,信息内容为用户id
	void sendGradRedPack(String userid);
}

src/main/java/com/sc/service/RedpackService.java

package com.sc.service;

public interface RedpackService {
	
	//查询红包个数
	int getRedPackRemain(Integer id);
	
	//扣减红包个数
	int deducteRedPack(Integer id);
	
}

src/main/java/com/sc/service/UserRedpackService.java

package com.sc.service;

import com.sc.entity.UserRedpack;

public interface UserRedpackService {

	//新增用户抢红包记录
	int insertGradReadPack(UserRedpack userRedpack);
}

src/main/java/com/sc/service/GrabRedPackService.java

package com.sc.service;

public interface GrabRedPackService {
	//抢红包业务
	void grabRedPack(String userId);
}

10.定义发送队列消息、红包、用户红包、抢红包业务Service接口具体实现
src/main/java/com/sc/service/impl/CommonMqServiceImpl.java

package com.sc.service.impl;    
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.sc.service.CommonMqService;

@Service
public class CommonMqServiceImpl implements CommonMqService{
	
	private static final Logger log = LoggerFactory.getLogger(CommonMqServiceImpl.class);

	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	/**
	 * 发送消息
	 */
	public void sendGradRedPack(String userid) {
		try {
			rabbitTemplate.convertAndSend("info", userid);
		} catch (AmqpException e) {
			log.error("发送用户抢红包进入消息队列异常:"+e.getMessage());
			e.printStackTrace();
		}
	}

}

src/main/java/com/sc/service/impl/RedpackServiceImpl.java

package com.sc.service.impl;

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

import com.sc.mapper.RedpackMapper;
import com.sc.service.RedpackService;

@Service
public class RedpackServiceImpl implements RedpackService{

	@Autowired
	private RedpackMapper redpackMapper;

	/**
	 * 获取红包剩余个数
	 * @param id
	 * @return
	 */
	public int getRedPackRemain(Integer id) {
		return redpackMapper.selectRemainByPrimaryKey(id);
	}

	/**
	 * 扣减红包剩余个数
	 */
	public int deducteRedPack(Integer id) {
		return redpackMapper.updateRemainRedPack(id);
	}

}

src/main/java/com/sc/service/impl/UserRedpackServiceImpl.java

package com.sc.service.impl;

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

import com.sc.entity.UserRedpack;
import com.sc.mapper.UserRedpackMapper;
import com.sc.service.UserRedpackService;

@Service
public class UserRedpackServiceImpl implements UserRedpackService{
	
	@Autowired
	private UserRedpackMapper userRedpackMapper;

	/**
	 * 插入用户抢红包记录
	 */
	public int insertGradReadPack(UserRedpack userRedpack) {
		return userRedpackMapper.insertSelective(userRedpack);
	}

}

src/main/java/com/sc/service/impl/GrabRedPackServiceImpl.java
主要的业务实现包括红包个数校验,更新红包剩余个数,插入用户抢红包记录

package com.sc.service.impl;

import java.math.BigDecimal;
import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.sc.entity.UserRedpack;
import com.sc.service.GrabRedPackService;
import com.sc.service.RedpackService;
import com.sc.service.UserRedpackService;

@Service
public class GrabRedPackServiceImpl implements GrabRedPackService{
	
	private static final Logger log = LoggerFactory.getLogger(GrabRedPackServiceImpl.class);
	
	private static final int redpackId = 1;
	private static final String amount = "10.00";
	
	@Autowired
	private RedpackService redpackService;
	@Autowired
	private UserRedpackService userRedpackService;

	public void grabRedPack(String userId) {
		try {
			//1.查询红包剩余个数是否大于0
			int remain = redpackService.getRedPackRemain(redpackId);
			if(remain > 0) {
				//2.扣减红包个数
				int result = redpackService.deducteRedPack(redpackId);
				if(result > 0) {
					//3.新增用户抢红包记录
					UserRedpack userRedpack = new UserRedpack();
					userRedpack.setUserid(userId);
					userRedpack.setRedpackid(redpackId);
					userRedpack.setGrabdate(new Date());
					userRedpack.setAmount(new BigDecimal(amount));
					userRedpackService.insertGradReadPack(userRedpack);
				}
				
			}
			//异步通知用户抢红包成功
		} catch (Exception e) {
			log.error("处理抢单异常:"+e.getMessage());
			throw new RuntimeException("处理抢单异常");
		}
	}
	
}

11.监听抢红包信息,调用抢红包业务方法
src/main/java/com/sc/consumer/RedPackConsumer.java

package com.sc.consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.beans.factory.annotation.Autowired;

import com.rabbitmq.client.Channel;
import com.sc.service.GrabRedPackService;

public class RedPackConsumer implements ChannelAwareMessageListener{
	
	private static final Logger log = LoggerFactory.getLogger(RedPackConsumer.class);
	
	@Autowired
	private GrabRedPackService grabRedPackService;

	/**
	 * 监听用户抢红包信息
	 */
	public void onMessage(Message message, Channel channel) throws Exception {
		long tag = message.getMessageProperties().getDeliveryTag();
		try {
			String userId = new String(message.getBody(),"UTF-8");
			log.info("监听到抢红包用户:"+userId);
			//执行抢红包业务
			grabRedPackService.grabRedPack(userId);
			//手动确认
			channel.basicAck(tag, false);
		} catch (Exception e) {
			e.printStackTrace();
			log.error("用户抢红包发生异常:"+e.getMessage());
			//拒绝接收
			channel.basicReject(tag, false);
		}
	}

}

12.抢红包请求控制器

src/main/java/com/sc/controller/GrabRedPackController.java
package com.sc.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.sc.entity.ResultModel;
import com.sc.service.CommonMqService;
import com.sc.service.impl.CommonMqServiceImpl;

@Controller
@RequestMapping("redpack")
public class GrabRedPackController {
	
	private static final Logger log = LoggerFactory.getLogger(CommonMqServiceImpl.class);
	
	@Autowired
	private CommonMqService commonMqService;
	
	@RequestMapping("grab")
	@ResponseBody
	public ResultModel<String> grab(String userid){
		try {
			commonMqService.sendGradRedPack(userid);
			return new ResultModel<String>(0, userid, "抢红包成功");
		} catch (Exception e) {
			e.printStackTrace();
			log.error("用户:"+userid+" 抢红包失败");
			return new ResultModel<String>(0, userid, "抢红包失败");
		}
	}

}

13.浏览器JS模拟消费者请求抢红包
这里模拟了1000个消费者抢红包。为了更好的测试可以提高并发量

<%@ 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>
<%
	String path = request.getContextPath();
%>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script src="<%=path%>/static/js/jquery-1.9.1.js"></script>
</head>
<body>
	<button onclick="test(2000)">抢红包测试</button>
</body>
<script type="text/javascript">
	function test(usernum){
		console.log("usernum:"+usernum);
		for(var i = 0; i < usernum; i++){
			console.log("i:"+i);
			$.post('<%=path%>/redpack/grab', {'userid':i},function(data){
				console.log(data);
			})
		}
	}
</script>
</html>

14.执行结果查看是否插入了1000条用户抢红包记录,结果如下,证明执行OK
在高并发量的情况下可能会出现超发的情况,这就需要后台加读写锁或者结合redis处理。本文就不做详细讲解,有时间后续在进一个深入。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/caiqing116/article/details/84640688