抢红包高并发demo2

开发控制器和超发现象测试

 首先完成的是抢红包控制器

//UserRedPacketController.java

package com.ssm.wdz.controller;

import java.util.HashMap;

import java.util.Map;

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

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import com.ssm.wdz.service.UserRedPacketService;

@Controller

@RequestMapping("/userRedPacket")

public class UserRedPacketController {

@Autowired

private UserRedPacketService userRedPacketService=null;

@RequestMapping(value="/grapRedPacket")

@ResponseBody

public Map grapRedPacket(Long redPacketId,Long userId){

//抢红包

int result=userRedPacketService.grapRedPacket(redPacketId, userId);

Map retMap=new HashMap();

boolean flag=result>0;

retMap.put("success", flag);

retMap.put("message", flag?"抢红包成功":"抢红包失败");

return retMap;

}

}

这样就完成了控制器的开发,对于控制器而言,它将抢夺一个红包,并且将一个Map返回,由于使用了注解@ResponseBody标注了方法,所以它最后会变成一个JSON返回前端,编写JSP对其进行测试,如下:

//test.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">

< !-- 加载Query文件 -->

< /script>

$(document).ready(function (){

//模拟30000个异步请求,进行并发

var max=30000;

for(var i=1;i<=max;i++){

//jQuery的post请求,请注意这是异步请求

$.post({

//请求抢红包id为1的红包

url:

"./userRedPacket/grapRedPacket.do?redPacketId=1&userId"+i,

//成功后的方法

success:function(result){

}

});

}

});

< /script>

< /head>

< /body>

< /html>

这里我们使用了javascript去模拟3万人同时抢红包的场景,在实际的测试中,使用FireFox浏览器测试。javascript的post请求是一个异步请求,所以这是一个高并发的场景,它将抢夺一个id为1的红包,依据之前的sql插入,这是一个20万元的红包,一共有2万个,那么这样的场景会有什么问题发生呢?要注意两个点:一个是数据的一致性,另一个就是性能问题。

在项目放到tomcat运行,报出了错误,如下:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRedPacketController': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.ssm.wdz.service.UserRedPacketService com.ssm.wdz.controller.UserRedPacketController.userRedPacketService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRedPacketServiceImpl': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.ssm.wdz.dao.UserRedPacketDao com.ssm.wdz.service.impl.UserRedPacketServiceImpl.userRedPacketDao; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.ssm.wdz.dao.UserRedPacketDao] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redPacketDao' defined in file [D:\PowerNet\apache-tomcat-7.0.85\webapps\redpacket\WEB-INF\classes\com\ssm\wdz\dao\RedPacketDao.class]: Cannot resolve reference to bean 'sqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class com.ssm.wdz.config.RootConfig: Invocation of init method failed; nested exception is org.apache.ibatis.builder.BuilderException: Error creating document instance.Cause: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 16; 文档根元素 "configuration" 必须匹配 DOCTYPE 根 "mapper"。

Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRedPacketDao' defined in file [D:\PowerNet\apache-tomcat-7.0.85\webapps\redpacket\WEB-INF\classes\com\ssm\wdz\dao\UserRedPacketDao.class]: Cannot resolve reference to bean 'sqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class com.ssm.wdz.config.RootConfig: Invocation of init method failed; nested exception is org.apache.ibatis.builder.BuilderException: Error creating document instance.Cause: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 16; 文档根元素 "configuration" 必须匹配 DOCTYPE 根 "mapper"。

Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redPacketDao' defined in file [D:\PowerNet\apache-tomcat-7.0.85\webapps\redpacket\WEB-INF\classes\com\ssm\wdz\dao\RedPacketDao.class]: Cannot resolve reference to bean 'sqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class com.ssm.wdz.config.RootConfig: Invocation of init method failed; nested exception is org.apache.ibatis.builder.BuilderException: Error creating document instance.Cause: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 16; 文档根元素 "configuration" 必须匹配 DOCTYPE 根 "mapper"。

Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRedPacketDao' defined in file [D:\PowerNet\apache-tomcat-7.0.85\webapps\redpacket\WEB-INF\classes\com\ssm\wdz\dao\UserRedPacketDao.class]: Cannot resolve reference to bean 'sqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class com.ssm.wdz.config.RootConfig: Invocation of init method failed; nested exception is org.apache.ibatis.builder.BuilderException: Error creating document instance.Cause: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 16; 文档根元素 "configuration" 必须匹配 DOCTYPE 根 "mapper"。

第一句写的大概就是自动注入失败,在网上找了好多,有说注解加错的,也有说要配置文件的。最后发现原来是,xml文档的头文件错了,这个还有区别。。。

在配置mybatis-config.xml的时候直接把mapper的xml头给拿过来了。。。下面贴出两个头:

//mapper.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">

//mybatis-config.xml

< ?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="com/ssm/wdz/mapper/UserRedPacket.xml"/>

< mapper resource="com/ssm/wdz/mapper/RedPacket.xml"/>

< /mappers>

< /configuration>

超发现象测试

 直接贴出bug

Error creating bean with name 'requestMappingHandlerAdapter' defined in class path resource [com/ssm/wdz/config/WebConfig.class]: Instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanDefinitionStoreException: Factory method [public org.springframework.web.servlet.HandlerAdapter com.ssm.wdz.config.WebConfig.initRequestMappingHandlerAdapter()] threw exception; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException

这里大约说的是在创建RequestMappingHandlerAdapter的Bean的时候失败,请注意这个ava.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException,

这个是指没有jar包或者是jar版本冲突,在网上找到了关于jackson的三个jar包,下面是链接

jackson

在运行起来后,进行请求

请求路径:http://localhost:8080/redpacket/userRedPacket/grapRedPacket.do?redPacketId=1&userId=1

报了一个错误

Cannot create JDBC driver of class 'com.mysql.jdbc.Driver' for connect URL 'jdbc://localhost:3306/redpacket'

这里说是不能创建jdbc驱动,仔细看了下连接数据库出的代码及RootConfig.java

发现 props.setProperty("url", "jdbc://localhost:3306/redpacket");

这一句漏了个mysql:

正确的如下:

props.setProperty("url", "jdbc:mysql://localhost:3306/redpacket");

重新进行测试

编写的jsp文件没有进行配置,我们在web.xml中加入如下代码:

 < welcome-file-list>

  < welcome-file>/WEB-INF/jsp/test.jsp

 < /welcome-file-list>

访问路径为:http://localhost:8080/redpacket/

由于电脑响应问题,最后把红包改为5000个,

最后控制台输出:

抢红包程序运行时间:150886ms,也就是150秒抢了5000个包。

但是刷新数据库表发现,在t_red_packet表中,stock减到了-1,也就是多发了一个红包,这就是高并发的超发现象,这是一个错误的逻辑。如果是较好的设备运行高并发可能会多发更多,现在看一下性能:

抢红包程序运行时间:521687ms,也就是抢20000个红包用了521秒,。。。该换电脑了啊。

超发现象是由多线程下数据不一致造成的,对于这类问题,当前互联网采用悲观锁和乐观锁来解决。

设计进阶

 对于之前的测试,产生了超发现象,超发现象是多线程数据不一致造成的,对于这种情况,目前只要采用悲观锁和乐观锁来处理。

悲观锁

  悲观锁是一种利用数据库内部机制提供锁的方法,也就是对更新的数据加锁。这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程就不能在对数据库进行更新了,这就是悲观锁。

首先在RedPacket.xml中修改代码:

< !-- 悲观锁实现查询红包具体信息 -->

< select id="getRedPacketForUpdate" parameterType="long" resultType="com.ssm.wdz.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} for update

< /select>

注意在sql中加入的 for update语句,意味着将持有对数据库记录的行更新锁(因为这里使用的是对主键查询,所以只会对行加锁,如果使用的是非主键查询,要考虑是否要对全表加锁的问题,加锁后可能会引发其他查询的阻塞),这就意味着在高并发的场景下,当一条事务持有了这个更新锁才能往下操作,其他的线程如果更新这条记录,都需要等待,这样就可以解决超发现象引发的数据一致性问题了。

同时在RedPacketDao中加入对应的查询方法。

/ **

*使用for update语句加锁

* @param id 红包id

* @return 红包信息

* /

public RedPacket getRedPacketForUpdate(Long id);

接下来将UserRedPacketImpl代码中加入

// 使用悲观锁方式获取红包信息

RedPacket redPacket=redPacketDao.getRedPacketForUpdate(redPacketId);

再次进行测试:

发现数据库中的数据已经不会减到0以下了,也就是说不会发生多发的现象。结果是正确的这点让人很欣喜,

这是之前运行的时间:

抢红包程序运行时间:369231ms

加了悲观锁之后的运行时间:

抢红包程序运行时间:364252ms

下面是数据库的区别:

很明显可以看出来,我的电脑进行测试这两种性能好像差不多,但是加了悲观锁之后,数据库的多发现象明显解决了,但是这个速度还是有点慢。现在可能是对数据库只是加了一个锁,当加的锁比较多的时候,数据库的性能还会持续下降,下面讨论一下性能下降的原因。

 对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候,CPU就会将这些得不到资源的线程挂起,挂起的资源会消耗CPU的资源,尤其是在高并发的请求中。

由于频繁挂起,等待持有锁线程释放资源,一旦释放资源后就开始抢夺,恢复线程,周而复始的将所有红包的资源抢完。在高并发的情况下,使用悲观锁就会造成大量的线程被挂起恢复,这将十分消耗资源,这就是为什么不怎么用悲观锁的原因。有些时候,我们也会把悲观锁称为独占锁,毕竟只有一个线程可以独占这个资源,或者称为阻塞锁,因为它会造成其他线程的阻塞。为了克服这个问题,程序大师们提出了乐观锁机制。

 乐观锁

  乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁实现它的设计里面由于不会阻塞其他线程,所以不会引发频繁的挂起和恢复,这样便能够提高并发性能,所以也称为非阻塞锁,乐观锁使用的是CAS原理。

CAS原理

 在CAS原理中,对于多个线程共同的资源,先保存一个旧值,需要操作数据的时候,先比较数据库当前的值和旧值是否一致,如果一直则进行操作数据的操作,否则就认为已经修改过了,不在进行操作。CAS原理并不排斥高并发同时也不独占资源,只是在线程开始阶段读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较。一致则更新,不一致则认为被其他线程更改过了,不再进行更新数据,可以考虑重试或者放弃。如果重试那么这就是一个重入锁,但是CAS会有一个问题,那就是ABA问题。

 ABA

  ABA问题主要是指,假设有两个线程,线程1先读入x=a,紧接着线程2读入x=a,在线程1处理业务逻辑的同时,线程2进行x=b操作,在处理自己的业务逻辑,紧接着又x=a,在下一个时间段线程1执行完业务逻辑进行判断,发现x=a,所以更新数据。由于业务逻辑存在回退的可能性,所以在线程2的值得改变为a-b-a,这就是ABA问题。

ABA问题的发生,是因为业务逻辑存在回退的可能性。如果加入一个非业务的属性,比如在数据库中加入版本号(version),对于版本号有一个约定,就是只要修改x的值,强制版本号只能递增,而不会回退,即便是其他业务回退,它也会递增,这个问题就解决了。

接下来,就通过乐观锁来完成抢红包业务。

乐观锁实现抢红包业务

 首先在RedPacket.xml中加入如下代码:

< !-- 通过版本号扣减抢红包

每进行一次更新,版本增1

其次增加对版本号的判断

-- >

< update id="decreaseRedPacketForVersion">

 update t_red_packet

 set stock=stock-1,

 version=version+1

 where id=#{id}

 and version=#{version}

< /update>

注意红色的代码,在扣减红包的时候,增加了对版本号的判断,每次扣减都会使版本号加1,以避免ABA问题,对于查询也不使用for update语句,避免锁的产生,这就没有线程阻塞的问题了,这里在对应的UserRedPackerDao接口上加入对应方法,然后就可以在类UserRedPacketServiceImpl中新增方法grapRedPacketForVersion(需要在UserRedPacketService加上相同的方法),完成对应的逻辑即可,代码如下:

//RedPacketDao.java

/ **

* 使用乐观锁实现扣减红包数

* @param id 红包id

* @param version 扣减版本

* @return 更新记录条数

* /

public int decreaseRedPacketForVersion(Long id,Integer version);

//UserRedPacketService.java

/ **

* 使用乐观锁保存红包信息

* @param redPacketId 红包编号

* @param userId 抢红包用户编号

* @return 影像记录条数

* /

public int grapRedPacketForVersion(Long redPacketId,Long userId);

//UserRedPacketServiceImpl.java

@Override

@Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)

public int grapRedPacketForVersion(Long redPacketId,Long userId) {

long startTime = System.currentTimeMillis();

//获取红包信息,注意version值

RedPacket redPacket=redPacketDao.getRedPacket(redPacketId);

//当前小红包库存大于0

if(redPacket.getStock()>0) {

//再次传入线程保存的version旧值给sql判断,是否有其他线程更改过数据

int update=redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());

//如果没有数据更新,则说明其他线程已经修改过数据,本次抢红包失败

if(update==0) {

return FAILED;

}

//生成抢红包信息

UserRedPacket userRedPacket=new UserRedPacket();

userRedPacket.setRedPacketId(redPacketId);

userRedPacket.setUserId(userId);

userRedPacket.setAmount(redPacket.getUnitAmount());

userRedPacket.setNote("redPacketId:"+redPacketId);

//插入抢红包信息

int result=userRedPacketDao.grapRedPacket(userRedPacket);

return result;

}else {

long endTime = System.currentTimeMillis(); 

//输出程序运行时间

System.out.println("抢红包程序运行时间:" + (endTime - startTime) + "ms");

//失败返回

return FAILED;

}

}

version值一开始就保存到了对象中,当扣减的时候,再次传递给sql,让sql对数据库的version和当前线程的旧值version进行比较。如果一直则插入抢红包数据,否则不进行操作。为了进行测试,在控制器UserRedPacketController内新建方法,代码如下:

@RequestMapping(value="/grapRedPacketForVersion")

@ResponseBody

public Map< String,Object > grapRedPakcetForVersion(Long redPacketId,Long userId){

//抢红包

int result=userRedPacketService.grapRedPacketForVersion(redPacketId, userId);

Map< String,Object > retMap=new HashMap();

boolean flag=result>0;

retMap.put("success", flag);

retMap.put("message", flag?"SUCCESS!!!":"FAILED");

return retMap;

}

将test.jsp的请求更改

" . / userRedPacket/grapRedPacketForVersion.do?redPacketId=3&userId="+i,

进行测试,报错:

严重: Servlet.service() for servlet [dispatcher] in context with path [/redpacket] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [0, 1, param1, param2]] with root cause

org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [0, 1, param1, param2]

大概就说传入的数据没找到,果断看了一眼含有参数version的RedPacketDaojava,果然是mybatis传入多个参数的时候不会自动映射,一个参数好像可以,代码更改如下,加入注解@Param

public int decreaseRedPacketForVersion(@Param("id") Long id,@Param("version") Integer version);

但是同样也产生了一个问题,在这里程序就不运行了

从图中可以看到,还剩509个红包没有抢完,但是程序已经停止了。这是红包因为版本高并发的原因没有抢到,而且概率还相当高,接下来解决这个问题。

为了克服这个问题,还会考虑使用重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入,会导致大量的sql运行,所以目前重入机制会加入两种限制,一种是按照时间戳的重入,也就是在一定时间戳内,不成功会循环到成功为止,直到超过时间戳,不成功才会退出,返回失败。另外一种是按次数,比如限定3次,程序尝试3次抢红包后,就判定请求失效,下面讨论如何重入。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容