热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

抢红包案例分析以及代码实现

概述电商的秒杀、抢购,春运抢票,微信QQ抢红包,从技术的角度来说,这对于Web系统是一个很大的考验.高并发场景下࿰

概述

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

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

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


抢红包案例

主要分以下几大部分:

  1. 环境搭建

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

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

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

  5. Redis实现抢红包


案例关注点

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

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


工程结构

640?wx_fmt=png


库表设计

MySql5.7

/*==============================================================*/
/* Table: 红包表                                        */
/*==============================================================*/
create table T_RED_PACKET
(
  id                   int(12)                        not null auto_increment COMMENT '红包编号',
  user_id              int(12)                        not null COMMENT '发红包的用户id',
  amount               decimal(16,2)                  not null COMMENT '红包金额',
  send_date            timestamp                      not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '发红包日期',
  total                int(12)                        not null COMMENT '红包总数'
  unit_amount          decimal(12)                    not null COMMENT '单个红包的金额',
  stock                int(12)                        not null COMMENT '红包剩余个数',
  version              int(12default 0              not null COMMENT '版本(为后续扩展用)',
  note                 varchar(256)                    null COMMENT '备注',,
  primary key clustered (id)
);

红包表表示存放红包的是一个大红包的信息,它会分为若干个小红包,为了业务简单,假设每一个红包是等额的。而对于抢红包而言,就是从大红包中抢夺那些剩余的小红包,剩余红包数会被记录在红包表中。 两个表有外键关联 T_RED_PACKET.id = T_USER_RED_PACKET.red_packet_id

/*==============================================================*/
/* Table: 用户抢红包表                                                */
/*==============================================================*/
create table T_USER_RED_PACKET 
(
  id                   int(12)                        not null auto_increment COMMENT '用户抢到的红包id',
  red_packet_id        int(12)                        not null COMMENT '红包id',
  user_id              int(12)                        not null COMMENT '抢红包用户的id',
  amount               decimal(16,2)                  not null  COMMENT '抢到的红包金额',
  grab_time            timestamp                      not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '抢红包时间',
  note                 varchar(256)                   null COMMENT '备注',
   primary key clustered (id)
);


/**
* 插入一个20万元金额,2万个小红包,每个10元的红包数据
*/

insert into T_RED_PACKET(user_id, amount, send_date, total, unit_amount, stock, note)
values(1200000.00now(), 2000010.0020000,'20万元金额,2万个小红包,每个10元');
commit;


这样就建好了两个表,并且将一个 20 万元金额,2 万个小红包,每个 10 元的红包信息插入到了红包表中,用作模拟数据。


Domain

有了这两个表,我们就可以为这两个表建两个 POJO 了,让这两个表和 POJO 对应起来,这两个 POJO 为 RedPacket 和 UserRedPacket,实现类序列化接口。

红包信息

package com.artisan.redpacket.pojo;

import java.io.Serializable;
import java.sql.Timestamp;

/**


@ClassName: RedPacket

@Description: 红包表对应的实体类,可序列化

@author: Mr.Yang

@date: 2018年10月8日 下午3:42:58
*/

public class RedPacket implements Serializable {

 private static final long serialVersiOnUID= 9036484563091364939L;
 // 红包编号
 private Long id;
 // 发红包的用户id
 private Long userId;
 // 红包金额
 private Double amount;
 // 发红包日期
 private Timestamp sendDate;
 // 红包总数
 private Integer total;
 // 单个红包的金额
 private Double unitAmount;
 // 红包剩余个数
 private Integer stock;
 // 版本(为后续扩展用)
 private Integer version;
 // 备注
 private String note;
 // 省略set/get
}


抢红包信息

package com.artisan.redpacket.pojo;

import java.io.Serializable;
import java.sql.Timestamp;

/**


@ClassName: UserRedPacket

@Description: 用户抢红包表

@author: Mr.Yang

@date: 2018年10月8日 下午3:47:40
*/

public class UserRedPacket implements Serializable {

 private static final long serialVersiOnUID= 7049215937937620886L;

 // 用户红包id
 private Long id;
 // 红包id
 private Long redPacketId;
 // 抢红包的用户的id
 private Long userId;
 // 抢红包金额
 private Double amount;
 // 抢红包时间
 private Timestamp grabTime;
 // 备注
 private String note;
 // 省略set/get
}


Dao层实现

MyBatis Dao接口类及对应的Mapper文件

使用 MyBatis 开发,先来完成大红包信息的查询先来定义一个 DAO 对象

package com.artisan.redpacket.dao;

import org.springframework.stereotype.Repository;

import com.artisan.redpacket.pojo.RedPacket;


@Repository
public interface RedPacketDao {
 
 /**
  * 获取红包信息.
  * @param id --红包id
  * @return 红包具体信息
  */

 public RedPacket getRedPacket(Long id);
 
 /**
  * 扣减抢红包数.
  * @param id -- 红包id
  * @return 更新记录条数
  */

 public int decreaseRedPacket(Long id);
 
 
}


其中的两个方法 , 一个是查询红包,另一个是扣减红包库存。

抢红包的逻辑是,先查询红包的信息,看其是否拥有存量可以扣减。如果有存量,那么可以扣减它,否则就不扣减。

接着将对应的Mapper映射文件编写一下

xml version="1.0" encoding="UTF-8" ?>
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.artisan.redpacket.dao.RedPacketDao">

 
 <select id="getRedPacket" parameterType="long"
   resultType="com.artisan.redpacket.pojo.RedPacket">

   select id, user_id as userId, amount, send_date as
   sendDate, total,
   unit_amount as unitAmount, stock, version, note from
   T_RED_PACKET
   where id = #{id}
 select>


 
 <update id="decreaseRedPacket">
   update T_RED_PACKET set stock = stock - 1 where id =
   #{id}
 update>


mapper>


这里getRedPacket并没有加锁这类动作,目的是为了演示超发红包的情况。

然后是抢红包的设计了 ,先来定义插入抢红包的 DAO ,紧接着是Mapper映射文件

package com.artisan.redpacket.dao;

import org.springframework.stereotype.Repository;

import com.artisan.redpacket.pojo.UserRedPacket;

@Repository
public interface UserRedPacketDao {

 /**
  * 插入抢红包信息.
  * @param userRedPacket ——抢红包信息
  * @return 影响记录数.
  */

 public int grapRedPacket(UserRedPacket  userRedPacket);
}


xml version="1.0" encoding="UTF-8" ?>
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.artisan.redpacket.dao.UserRedPacketDao">
   
   <insert id="grapRedPacket" useGeneratedKeys="true" 
   keyProperty="id" parameterType="com.artisan.redpacket.pojo.UserRedPacket">

     insert into T_USER_RED_PACKET( red_packet_id, user_id, amount, grab_time, note)
     values (#{redPacketId}, #{userId}, #{amount}, now(), #{note}) 
   insert>

mapper>


这里使用了 useGeneratedKeys 和 keyPrope町,这就意味着会返回数据库生成的主键信息,这样就可以拿到插入记录的主键了 , 关于 DAO 层就基本完成了。别忘了单元测试!!!


Service层实现

接下来定义两个 Service 层接口,分别是 UserRedPacketService和RedPacketService

package com.artisan.redpacket.service;

import com.artisan.redpacket.pojo.RedPacket;


public interface RedPacketService {
 
 /**
  * 获取红包
  * @param id ——编号
  * @return 红包信息
  */

 public RedPacket getRedPacket(Long id);

 /**
  * 扣减红包
  * @param id——编号
  * @return 影响条数.
  */

 public int decreaseRedPacket(Long id);
 
}


package com.artisan.redpacket.service;

public interface UserRedPacketService {
 
 /**
  * 保存抢红包信息.
  * @param redPacketId 红包编号
  * @param userId 抢红包用户编号
  * @return 影响记录数.
  */

 public int grapRedPacket(Long redPacketId, Long userId);
 
}


实现类如下:

package com.artisan.redpacket.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.artisan.redpacket.dao.RedPacketDao;
import com.artisan.redpacket.pojo.RedPacket;
import com.artisan.redpacket.service.RedPacketService;

@Service
public class RedPacketServiceImpl implements RedPacketService {
 
 @Autowired
 private RedPacketDao redPacketDao;

 @Override
 @Transactional(isolation=Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
 public RedPacket getRedPacket(Long id) {
   return redPacketDao.getRedPacket(id);
 }

 @Override
 @Transactional(isolation=Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
 public int decreaseRedPacket(Long id) {
   return redPacketDao.decreaseRedPacket(id);
 }

}


配置了事务注解@Transactional , 让程序能够在事务中运行,以保证数据的一致性 , 这里采用的是读/写提交的隔离级别 , 之所以不采用更高的级别, 主要是提高数据库的并发能力,而对于传播行为则采用 Propagation.REQUIRED,这样调用这个方法的时候,如果没有事务则会创建事务, 如果有事务则沿用当前事务。

实现 UserRedPacketService 接口的方法 grapRedPacket,它是核心的接口方法

package com.artisan.redpacket.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.artisan.redpacket.dao.RedPacketDao;
import com.artisan.redpacket.dao.UserRedPacketDao;
import com.artisan.redpacket.pojo.RedPacket;
import com.artisan.redpacket.pojo.UserRedPacket;
import com.artisan.redpacket.service.UserRedPacketService;

@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {
 
 // private Logger logger =
 // LoggerFactory.getLogger(UserRedPacketServiceImpl.class);
 
 @Autowired
 private UserRedPacketDao userRedPacketDao;

 @Autowired
 private RedPacketDao redPacketDao;

 // 失败
 private static final int FAILED = 0;

 @Override
 @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
 public int grapRedPacket(Long redPacketId, Long userId) {
   // 获取红包信息
   RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
   int leftRedPacket = redPacket.getStock();
   // 当前小红包库存大于0
   if (leftRedPacket > 0) {
     redPacketDao.decreaseRedPacket(redPacketId);
     // logger.info("剩余Stock数量:{}", leftRedPacket);
     // 生成抢红包信息
     UserRedPacket userRedPacket = new UserRedPacket();
     userRedPacket.setRedPacketId(redPacketId);
     userRedPacket.setUserId(userId);
     userRedPacket.setAmount(redPacket.getUnitAmount());
     userRedPacket.setNote("redpacket- " + redPacketId);
     // 插入抢红包信息
     int result = userRedPacketDao.grapRedPacket(userRedPacket);
     return result;
   }
   // logger.info("没有红包啦.....剩余Stock数量:{}", leftRedPacket);
   // 失败返回
   return FAILED;
 }


}


grapRedPacket 方法的逻辑是首先获取红包信息,如果发现红包库存大于 0,则说明还有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。要注意的是,数据库事务方面的设置,代码中使用注解@Transactional , 说明它会在一个事务中运行,这样就能够保证所有的操作都是在一个事务中完成的。在高并发中会发生超发的现象,后面会看到超发的实际测试。


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

我们这里将使用注解的方式来完成 SSM 开发的环境,可以通过继承 AbstractAnnotationConfigDispatcherServletlnitfal izer 去配置其他内 容,因此首先来配置 WebApplnitialize

package com.artisan.redpacket.config;

import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

 // Spring IoC环境配置
 @Override
 protected Class[] getRootConfigClasses() {
   // 配置Spring IoC资源
   return new Class[] { RootConfig.class };
 }

 // DispatcherServlet环境配置
 @Override
 protected Class[] getServletConfigClasses() {
   // 加载Java配置类
   return new Class[] { WebConfig.class };
 }

 // DispatchServlet拦截请求配置
 @Override
 protected String[] getServletMappings() {
   return new String[] { "*.do" };
 }

 /**
  * @param dynamic
  *            Servlet上传文件配置.
  */

 @Override
 protected void customizeRegistration(Dynamic dynamic) {
   // 配置上传文件路径
   String filepath = "D:/";
   // 5MB
   Long singleMax = (long) (5 * Math.pow(220));
   // 10MB
   Long totalMax = (long) (10 * Math.pow(220));
   // 设置上传文件配置
   dynamic.setMultipartConfig(new MultipartConfigElement(filepath, singleMax, totalMax, 0));
 }

}


WebAppInitializer继承了 AbstractAnnotationConfigDispatcherServletlnitializer, 重写了 3 个抽象方法 , 并且覆盖了父类的 customizeRegistration 方法 , 作为上传文件的配置。

  • getRootConfigClasses 是一个配置 Spring IoC 容器的上下文配置 , 此配置在代码中将会由类 RootConfig 完成

  • getServletConfigClasses 配置 DispatcherServlet 上下文配置,将会由WebConfig完成

  • getServletMappings 配置 DispatcherServlet 拦截 内 容 , 这里设置的是拦截所有以 .do 结尾的请求

通过这 3 个方法就可以配置 Web 工程中 的 Spring IoC 资源和 DispatcherServlet 的配置内容 , 首先是配置 Spring IoC 容器,配置类 RootConfig

package com.artisan.redpacket.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;

@Configuration
//定义Spring 扫描的包
@ComponentScan(value= "com.*", includeFilters= {@Filter(type = FilterType.ANNOTATION, value ={Service.class})})
//使用事务驱动管理器
@EnableTransactionManagement
//实现接口TransactionManagementConfigurer,这样可以配置注解驱动事务
public class RootConfig implements TransactionManagementConfigurer {
 
 private DataSource dataSource = null;
 
 /**
  * 配置数据库.
  * @return 数据连接池
  */

 @Bean(name = "dataSource")
 public DataSource initDataSource() {
   if (dataSource != null) {
     return dataSource;
   }
   try {
     Properties props = new Properties();
     props.load(RootConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"));
     props.setProperty("driverClassName", props.getProperty("jdbc.driver"));
     props.setProperty("url", props.getProperty("jdbc.url"));
     props.setProperty("username", props.getProperty("jdbc.username"));
     props.setProperty("password", props.getProperty("jdbc.password"));
     dataSource = BasicDataSourceFactory.createDataSource(props);
   } catch (Exception e) {
     e.printStackTrace();
   }
   return dataSource;
 }
 
 /***
  * 配置SqlSessionFactoryBean
  * @return SqlSessionFactoryBean
  */

 @Bean(name="sqlSessionFactory")
 public SqlSessionFactoryBean initSqlSessionFactory() {
   SqlSessionFactoryBean sqlSessiOnFactory= new SqlSessionFactoryBean();
   sqlSessionFactory.setDataSource(initDataSource());
   //配置MyBatis配置文件
   Resource resource = new ClassPathResource("mybatis/mybatis-config.xml");
   sqlSessionFactory.setConfigLocation(resource);
   return sqlSessionFactory;
 }
 
 /***
  * 通过自动扫描,发现MyBatis Mapper接口
  * @return Mapper扫描器
  */

 @Bean 
 public MapperScannerConfigurer initMapperScannerConfigurer() {
   MapperScannerConfigurer msc = new MapperScannerConfigurer();
   msc.setBasePackage("com.*");
   msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
   msc.setAnnotationClass(Repository.class);
   return msc;
 }
 
 
 /**
  * 实现接口方法,注册注解事务,当@Transactional 使用的时候产生数据库事务 
  */

 @Override
 @Bean(name="annotationDrivenTransactionManager")
 public PlatformTransactionManager annotationDrivenTransactionManager() {
   DataSourceTransactionManager transactiOnManager= 
          new DataSourceTransactionManager();
   transactionManager.setDataSource(initDataSource());
   return transactionManager;
 }
 
 

}


这个类和之前论述的有所不同 , 它标注了注解@EnableTransactionManagement , 实现了接口 TransactionManagementConfigurer, 这样的配置是为了实现注解式的事务 , 将来可以通过注解@Transactional 配 置数据库事务。

它有一 个方法annotationDrivenTransactionManager这需要将一个事务管理器返回给它就可以了

除了配置数据库事务外 ,还配置了数据源 SqISessionFactoryBean 和 MyBatis 的扫描类 , 并把 MyBatis的扫描类通过注解@Repository 和包名"com.*"限定。这样 MyBatis 就会通过 Spring 的机制找到对应的接 口和配置 , Spring 会自动把对应的接口装配到 IoC 容器中 。

有了 Spring IoC 容器后 , 还需要配置 DispatcherServlet 上下文

package com.artisan.redpacket.config;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
//定义Spring MVC扫描的包
@ComponentScan(value="com.*", includeFilters= {@Filter(type = FilterType.ANNOTATION, value = Controller.class)})
//启动Spring MVC配置
@EnableWebMvc
public class WebConfig

 /***
  * 通过注解 @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 mediaTypes = new ArrayList();
   mediaTypes.add(mediaType);
   //加入转换器的支持类型
   jsonConverter.setSupportedMediaTypes(mediaTypes);
   //往适配器加入json转换器
   rmhd.getMessageConverters().add(jsonConverter);
   return rmhd;
 }
}


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

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

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

xml version="1.0" encoding="UTF-8" ?>
 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
   <mappers>
       <mapper resource="mapper/UserRedPacket.xml"/>
       <mapper resource="mapper/RedPacket.xml"/>
   mappers>

configuration>


记得进行Service层的单元测试, 关于后台的逻辑就已经完成 , 接下来继续将Controller层实现,进行页面测试吧。


Controller层

package com.artisan.redpacket.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.artisan.redpacket.service.UserRedPacketService;

@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {

 @Autowired
 private UserRedPacketService userRedPacketService;

 @RequestMapping(value = "/grapRedPacket")
 @ResponseBody
 public Map<StringObject> grapRedPacket(Long redPacketId, Long userId) {
   // 抢红包
   int result = userRedPacketService.grapRedPacket(redPacketId, userId);
   Map<StringObject> retMap = new HashMap<StringObject>();
   boolean flag = result > 0;
   retMap.put("success", flag);
   retMap.put("message", flag ? "抢红包成功" : "抢红包失败");
   return retMap;
 }  
}


对于控制器而言 , 它将抢夺一个红包 , 并且将一个 Map返回,由于使用了注解@ResponseBody 标注方法,所以最后它会转变为一个 JSON 返回给前端请求,编写 JSP 对其进行测试


View层

grap.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>


<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
       <title>参数title>
       
       <script type="text/Javascript" src="https://code.jquery.com/jquery-3.2.0.js">
       script>
       <script type="text/Javascript">
           $(document).ready(function () {
             //模拟30000个异步请求,进行并发
             var max = 30000;
             for (var i = 1; i <= max; i++) {
                 //jQuery的post请求,请注意这是异步请求
                 $.post({
                     //请求抢id为1的红包
                     //根据自己请求修改对应的url和大红包编号
                     url: "./userRedPacket/grapRedPacket.do?redPacketId=1&userId=" + i,
                     //成功后的方法
                     success: function (result{
                     }
                 });
             }
         });
       script>
   head>
   <body>
   body>
html>


这里我们使用了 Javascript 去模拟 3 万人同时抢红包的场景 . 请使用 Firefox进行测试(Chrome老是丢失请求,IE慢)

Javascript 的 post 请求是一个异步请求,所以这是一个高并发的场景,它将抢夺 id 为1的红包 , 依据之前 SQL 的插入 , 这是一个 20 万元的红包 , 一共有两万个,那么在这样高并发场景下会有什么问题发生呢?

注意两个点 : 一个是数据的一致性,另外一个是性能问题


运行测试

启动tomcat,前端访问 http://localhost:8080/ssm_redpacket/grap.jsp

如果有日志,记得调成error级别,或者不打印日志。

我这里的mysql是部署在虚拟机中,CPU和内存的配置都不高。 内存1G。


超量发送的BUG验证

模拟高并发场景的抢红包后,两个维度进行统计

  • 1:数据一致性

  • 2: 性能


抢红包一致性统计:

SELECT
 a.id,
 a.amount,
 a.stock
FROM
 T_RED_PACKET a
WHERE
 a.id = 1
UNION ALL
 SELECT
   max(b.user_id),
   sum(b.amount),
   count(*)
 FROM
   T_USER_RED_PACKET b
 WHERE
   b.red_packet_id = 1;

640?wx_fmt=png

使用 SQL 去查询红包的库存、发放红包的总个数、总金额,我们发现了错误,红包总额为 20 万元,两万个小红包,结果发放了 200020元的红包, 20002 个红包。现有库存为-2,超出了之前的限定,这就是高并发的超发现象,这是一个错误的逻辑 。


抢红包性能统计:

SELECT
 (
   UNIX_TIMESTAMP(max(a.grab_time)) - UNIX_TIMESTAMP(min(a.grab_time)) 
 )  AS lastTime
FROM
 T_USER_RED_PACKET a;

640?wx_fmt=png

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


超发问题解决思路

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

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


代码

https://github.com/yangshangwei/ssm_redpacket


扩展阅读

CAS 实现 SSO 单点登录

ssh 服务突然连接不了案例总结

Java实现贪吃蛇小游戏(附完整源码)


来源:https://blog.csdn.net/yangshangwei/article/details/82975845

文章来源网络,版权归作者本人所有,如侵犯到原作者权益,请与我们联系删除

640?wx_fmt=jpeg



推荐阅读
  • MySQL 5.7 学习指南:SQLyog 中的主键、列属性和数据类型
    本文介绍了 MySQL 5.7 中主键(Primary Key)和自增(Auto-Increment)的概念,以及如何在 SQLyog 中设置这些属性。同时,还探讨了数据类型的分类和选择,以及列属性的设置方法。 ... [详细]
  • MySQL初级篇——字符串、日期时间、流程控制函数的相关应用
    文章目录:1.字符串函数2.日期时间函数2.1获取日期时间2.2日期与时间戳的转换2.3获取年月日、时分秒、星期数、天数等函数2.4时间和秒钟的转换2. ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • 数据类型和操作数据表2.1MySQL类型之整型2.2MySQL数据类型之浮点型2.3日期时间型DATE1支持时间:1000年1月1日~9999年12月31日DATETIME ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • 本文详细介绍如何在忘记MySQL服务器密码的情况下进行密码重置,包括具体的步骤和注意事项。 ... [详细]
  • 一个建表一个执行crud操作建表代码importandroid.content.Context;importandroid.database.sqlite.SQLiteDat ... [详细]
  • 在 CentOS 7 环境中使用 MySQL 5.6 镜像启动数据库时遇到权限问题,本文将详细探讨并提供解决方案。 ... [详细]
  • php更新数据库字段的函数是,php更新数据库字段的函数是 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文详细介绍了MySQL数据库的基础语法与核心操作,涵盖从基础概念到具体应用的多个方面。首先,文章从基础知识入手,逐步深入到创建和修改数据表的操作。接着,详细讲解了如何进行数据的插入、更新与删除。在查询部分,不仅介绍了DISTINCT和LIMIT的使用方法,还探讨了排序、过滤和通配符的应用。此外,文章还涵盖了计算字段以及多种函数的使用,包括文本处理、日期和时间处理及数值处理等。通过这些内容,读者可以全面掌握MySQL数据库的核心操作技巧。 ... [详细]
  • MySQL Decimal 类型的最大值解析及其在数据处理中的应用艺术
    在关系型数据库中,表的设计与SQL语句的编写对性能的影响至关重要,甚至可占到90%以上。本文将重点探讨MySQL中Decimal类型的最大值及其在数据处理中的应用技巧,通过实例分析和优化建议,帮助读者深入理解并掌握这一重要知识点。 ... [详细]
  • PTArchiver工作原理详解与应用分析
    PTArchiver工作原理及其应用分析本文详细解析了PTArchiver的工作机制,探讨了其在数据归档和管理中的应用。PTArchiver通过高效的压缩算法和灵活的存储策略,实现了对大规模数据的高效管理和长期保存。文章还介绍了其在企业级数据备份、历史数据迁移等场景中的实际应用案例,为用户提供了实用的操作建议和技术支持。 ... [详细]
  • 在本文中,我们将探讨如何在Docker环境中高效地管理和利用数据库。首先,需要安装Docker Desktop以确保本地环境准备就绪。接下来,可以从Docker Hub中选择合适的数据库镜像,并通过简单的命令将其拉取到本地。此外,我们还将介绍如何配置和优化这些数据库容器,以实现最佳性能和安全性。 ... [详细]
  • 本文详细介绍了在 Oracle 数据库中使用 MyBatis 实现增删改查操作的方法。针对查询操作,文章解释了如何通过创建字段映射来处理数据库字段风格与 Java 对象之间的差异,确保查询结果能够正确映射到持久层对象。此外,还探讨了插入、更新和删除操作的具体实现及其最佳实践,帮助开发者高效地管理和操作 Oracle 数据库中的数据。 ... [详细]
author-avatar
堕天使乖怪_546
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有