高并发抢红包案列以及使用锁,版本号,redis缓存解决,项目可运行,详细注释(一)

 1.问题描述

简单来说就是当大量数据来访问数据库的时候,可能导致数据不一致。如下:

发一个2000元的大红包,总共2000个小红包,每个一元,但是有30000个人去抢,红包少一个就减一,插入抢红包用户信息,结果看图:

 stock表示余留的红包数,结果是负一

 

看见那个2001,居然有2001个人抢到了红包,这就是问题所在了。

接下来我会给出整个项目,和讲解。

2.建立表

红包表:

create table T_RED_PACKET
(
id int(12) not null auto_increment,
user_id int(12) not null,
amount decimal(16,2) not null,
send_date timestamp not null,
total int(12) not null,
unit_amount decimal(12) not null,
stock int(12) not null,
version int(12) default 0 not null,
note varchar(256) null,
primary key clustered(id)
);

amount:总红包金额大小

扫描二维码关注公众号,回复: 3990626 查看本文章

total:总个数   stock:余留红包数  version:版本号

抢红包的用户表:

create table T_USER_RED_PACKET
(
id int(12) not null auto_increment,
red_packet_id int(12) not null,
user_id int(12) not null,
amount decimal(16,2) not null,
grab_time timestamp not null,
note varchar(256) null,
primary key clustered (id)
);

red_pack_id:上一张表的id

 插入红包:

insert into T_RED_PACKET(user_id,amount,send_date,total,unit_amount,stock,note)
values(1,2000.00 , now(),2000,1.00,2000,'2000元金额,2000个小红包,每个1元');

 其实大家的英文都看得懂吧;

3目录结构

 这采用了注解开发的模式,当然用xml配置是一样的,你也可以用springboot,但是原理都一样的

config:配置文件

dao:sql语句

pojo:对象

service:具体的逻辑

4.详细文件

RootConfig.java

package test814RedPacket.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
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;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月13日
 */
@Configuration
//定义 Spring 扫描的包
@ComponentScan(value="test814RedPacket.*",includeFilters={@Filter(type=FilterType.ANNOTATION,value={Service.class})})
//使用事务驱动管理器
@EnableTransactionManagement
//实现接口 TransactionManagementConfigurer ,这样可以配置注解驱动事务
public class RootConfig  implements TransactionManagementConfigurer{
    
    
    private DataSource dataSource = null;
    
    
    
    /**
     * 设置日志
     * @Description 这里有个坑,log4j的配置文件得放到源文件加的更目录下,src下才起作用,放包里不起作用,找了好久的错误
     * @Param
     * @Return
     */
    @Bean(name="PropertiesConfigurer")
    public PropertyPlaceholderConfigurer initPropertyPlaceholderConfigurer(){
        PropertyPlaceholderConfigurer propertyLog4j = new PropertyPlaceholderConfigurer();
        Resource resource = new  ClassPathResource("log4j.properties");
        propertyLog4j.setLocation(resource);
        return propertyLog4j;
    }
    
    
    /**
     * 配置数据库
     */
    @Bean(name="dataSource")
    public DataSource initDataSource(){
        if(dataSource!=null){
            return dataSource;
        }
        Properties props = new Properties();
        props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/t_role");
        props.setProperty("username","root");
        props.setProperty("password", "123456");
        props.setProperty("maxActive", "200");
        props.setProperty("maxIdle", "20");
        props.setProperty("maxWait", "30000");
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
    /**
     * 配置 SqlSessionFactoryBean,这里引入了spring-mybatis的jar包,是两个框架的整合
     */
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactoryBean initSqlSessionFactory(){
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource);
        //配置 MyBatis 配置文件
        Resource resource = new  ClassPathResource("test814RedPacket/config/mybatis-config.xml");
        sqlSessionFactory.setConfigLocation(resource);
        return sqlSessionFactory;
    }
    

    
    /**
     * 通过自动扫描,发现 MyBatis Mapper 接口
     */
    @Bean
    public MapperScannerConfigurer initMapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        //扫描包
        msc.setBasePackage("test814RedPacket.*");
        msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
        //区分注解扫描
        msc.setAnnotationClass(Repository.class);
        return msc;
    }
    
    /**
     * 实现接口方法,注册注解事务 当@Transactonal 使用的时候产生数据库事务
     */
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(initDataSource());
        return transactionManager;
    }

}

 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
       <mapper resource="test814RedPacket/dao/RedPacketmapping.xml" />
       <mapper resource="test814RedPacket/dao/UserRedPacketmapping.xml" />
    </mappers>
</configuration>

RedPacketMapper.java

package test814RedPacket.dao;

import org.springframework.stereotype.Repository;

import test814RedPacket.pojo.RedPacket;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月14日
 */
@Repository
public interface RedPacketMapper {

    /**
     * 获取红包信息
     * @Description
     * @Param
     * @Return
     */
    public RedPacket getRedPacket(int id);
    
    /**
     * 扣减抢红包数
     */
    public int decreaseRedPacket(int id);
    
    
    /**
     * 其中的两个方法 1个是查询红包,另 1个是扣减红包库存。抢红包的逻辑是,先
询红包的信息,看其是否拥有存量可以扣减。如果有存量,那么可以扣减它,否则就不扣
减,现在用 个映射 ML 实现这两个方法
     */
}

 UserRedPacketMapper.java

package test814RedPacket.dao;

import org.springframework.stereotype.Repository;

import test814RedPacket.pojo.UserRedPacket;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */
@Repository
public interface UserRedPacketMapper {

    /**
     * 插入抢红包信息
     */
    public int grapRedPacket(UserRedPacket userRedPacket);
}

 RedPacketMapping

<?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="test814RedPacket.dao.RedPacketMapper">

<!-- 查询红包具体信息 -->
<select id="getRedPacket" parameterType="int"
    resultType="test814RedPacket.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>
   

 UserRedPacktMapping.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="test814RedPacket.dao.UserRedPacketMapper">

    <!-- 插入抢红包信息   这里使用了 useGeneratedKeys keyPrope町,这就意味着会返回数据库生成 主键信
息,这样就可以拿到插入记录的主键了 关于 DAO 层就基本完成了。 -->
    <insert id="grapRedPacket" useGeneratedKeys="true" keyProperty="id"
        parameterType="test814RedPacket.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>
   

对象:dao

RedPacket.java

package test814RedPacket.pojo;

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


/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月14日
 */
//实现 Serializable 接口 ,这样便可序列化对象
public class RedPacket implements Serializable{

    /**
     *
     */
    private static final Long serialVersionUID = -2257220618244092741L;

    

    private int id;
    private int userId;
    private Double amount;
    private Timestamp sendDate;
    private int total;
    private Double unitAmount;
    private int stock;
    private int version;
    private String note;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getUserId() {
        return userId;
    }
    public void setUserId(int userId) {
        this.userId = userId;
    }
    public Double getAmount() {
        return amount;
    }
    public void setAmount(Double amount) {
        this.amount = amount;
    }
    public Timestamp getSendDate() {
        return sendDate;
    }
    public void setSendDate(Timestamp sendDate) {
        this.sendDate = sendDate;
    }
    public int getTotal() {
        return total;
    }
    public void setTotal(int total) {
        this.total = total;
    }
    public Double getUnitAmount() {
        return unitAmount;
    }
    public void setUnitAmount(Double unitAmount) {
        this.unitAmount = unitAmount;
    }
    public int getStock() {
        return stock;
    }
    public void setStock(int stock) {
        this.stock = stock;
    }
    public int getVersion() {
        return version;
    }
    public void setVersion(int version) {
        this.version = version;
    }
    public String getNote() {
        return note;
    }
    public void setNote(String note) {
        this.note = note;
    }
    
    
}

 UserRedPacket.java

package test814RedPacket.pojo;

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

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月14日
 */

public class UserRedPacket implements Serializable{

    /**
     *
     */
    private static final long serialVersionUID = -8420747405164675025L;

    
    private int id;
    private int redPacketId;
    private int userId;
    private Double amount;
    private Timestamp grabTime;
    private String note;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getRedPacketId() {
        return redPacketId;
    }
    public void setRedPacketId(int redPacketId) {
        this.redPacketId = redPacketId;
    }
    public int getUserId() {
        return userId;
    }
    public void setUserId(int userId) {
        this.userId = userId;
    }
    public Double getAmount() {
        return amount;
    }
    public void setAmount(Double amount) {
        this.amount = amount;
    }
    public Timestamp getGrabTime() {
        return grabTime;
    }
    public void setGrabTime(Timestamp grabTime) {
        this.grabTime = grabTime;
    }
    public String getNote() {
        return note;
    }
    public void setNote(String note) {
        this.note = note;
    }
    
    
}

 service:

RedPacketServiceimpl.java

package test814RedPacket.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 test814RedPacket.dao.RedPacketMapper;
import test814RedPacket.pojo.RedPacket;
import test814RedPacket.service.inf.RedPacketService;

/**
 * @Description
 * 配置了事务注解@Transactional 让程序 够在事务中运 ,以保证数据 致性
里采用的是读/写提交 隔离级别.
所以不采用更高 别, 主要是提高数据库 并发
力,而对于传播行为 采用 Propagation.REQUIRED ,这 用这 方法的时 ,如果没有
事务则会 建事务, 果有事务 沿用当前事务。
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */
@Service
public class RedPacketServiceimpl implements RedPacketService{
    
    
    @Autowired
    private RedPacketMapper redPacketMapper;
    
    
    
 
    @Override
    @Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
    public RedPacket getRedPacket(int id) {
        return redPacketMapper.getRedPacket(id);
    }

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

}

 UserRedPacketServiceimpl.java

package test814RedPacket.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 test814RedPacket.dao.RedPacketMapper;
import test814RedPacket.dao.UserRedPacketMapper;
import test814RedPacket.pojo.RedPacket;
import test814RedPacket.pojo.UserRedPacket;
import test814RedPacket.service.inf.UserRedPacketService;

/**
 * @DescriptiongrapRedPacket 方法的逻辑是首先获取红包信息,如果发现红包库存大于 ,则说明
有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。要注意的是,数据库事
务方面的设置,代码中使用注解@Transactional 说明它会在 个事务中运行,这样就能够
保证所有的操作都是在-个事务中完成的。在高井发中会发生超发的现象,后面会看到超
发的实际测试。
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */
@Service
public class UserRedPacketServiceimpl implements UserRedPacketService{
    
    @Autowired
    private UserRedPacketMapper userRedPacketMapper;
    
    @Autowired
    private RedPacketMapper redPacketMapper;
    
    
    private static final int FAILED = 0;
    
    
    @Override
    //@Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
    public int grabRedPacket(int redPacketId, int userId) {
        //获取红包信息
        RedPacket redPacket = redPacketMapper.getRedPacket(redPacketId);
        //当前红包数大于0
        if(redPacket.getStock()>0){
            redPacketMapper.decreaseRedPacket(redPacketId);
            //生成抢红包信息
            UserRedPacket userRedPacket = new UserRedPacket();
            userRedPacket.setRedPacketId(redPacketId);
            userRedPacket.setUserId(userId);
            userRedPacket.setAmount(redPacket.getUnitAmount());
            userRedPacket.setNote("抢红包"+redPacketId);
            //插入抢红包信息
            int result = userRedPacketMapper.grapRedPacket(userRedPacket);
            return result ;            
        }
        
        return FAILED;
    }
}

RedPacketService

package test814RedPacket.service.inf;

import test814RedPacket.pojo.RedPacket;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */

public interface RedPacketService {

    /**
     * 获取红包
     * @Description
     * @Param
     * @Return
     */
    public RedPacket getRedPacket(int id);
    
    
    /**
     * 扣减红包
     */
    public int decreaseRedPacket(int id);
}

 UserRedPacketService

package test814RedPacket.service.inf;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */

public interface UserRedPacketService {

    /**
     * 保存抢红包信息
     * @Description
     * @Param
     * @Return
     */
    public int grabRedPacket(int redPacketId,int userId);
}

 log4j.properties

log4j.rootLogger = DEBUG,stdout
log4j.logger.org.springframework=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n

 测试:

package test814RedPacket;

import java.sql.Date;

import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import test814RedPacket.config.RootConfig;
import test814RedPacket.service.impl.UserRedPacketServiceimpl;
import test814RedPacket.service.inf.UserRedPacketService;

/**
 * @Description
 * @Author zengzhiqiang
 * @Date 2018年8月15日
 */

public class Test814Main {


    
    
    private static Logger log = Logger.getLogger(Test814Main.class);
    
    @SuppressWarnings("resource")
    public static void main(String[] args) {
        
        log.info("begin....");
        
        final int packet =9;
        
        //使用注解 Spring IoC 容器
        ApplicationContext ctx = new AnnotationConfigApplicationContext(RootConfig.class);        
        
        //获取角色服务类 "./userRedPacket/grapRedPacket.do?redPacketid=1&userid=" +i, userPacketService.grabRedPacket(redPacketId, userId);
        final UserRedPacketService roleService = ctx.getBean(UserRedPacketService.class);
        
        Date start = new Date(System.currentTimeMillis());
        

        Thread t = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                for (int i = 0; i < 5000; i++) {
                    roleService.grabRedPacket(packet, i);
                }
            }
        });

        t.start();
        
        
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                for (int i = 0; i < 6000; i++) {
                    roleService.grabRedPacket(packet, i);
                }
            }
        });

        t1.start();
        
        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                for (int i = 0; i < 8000; i++) {
                    roleService.grabRedPacket(packet, i);
                }
            }
        });

        t2.start();
        
        
        for (int i = 0; i < 900; i++) {
            roleService.grabRedPacket(packet, i);
        }
        Date end = new Date(System.currentTimeMillis());
        System.out.println("operation ok");
        System.out.println("开始时间:"+start);
        System.out.println("结束时间"+end);
    }
    

}

 好了,代码都在了,讲解下:

测试中的

final int packet =9; 是你要抢的哪个红包,

就是数据库中的红包id,

我启动了4个线程,

    Thread t = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                for (int i = 0; i < 5000; i++) {
                    roleService.grabRedPacket(packet, i);
                }
            }
        });

        t.start();

其中一个,5000是这个线程有5000个人去抢,虽然只有2000个红包,结果是0 没有超。

有时候会超的,比如上图的-1,-2就是超了的

这里有个坑,抢的人多谢才能超的,我之前一直没出问题,就是感觉人少了,我还以为是我的代码问题

大家也可以看看这篇文章:有源码的

https://blog.csdn.net/qq_33764491/article/details/81083644

这是spring写的,下个gradle,安插件,导入

下一篇介绍下解决

猜你喜欢

转载自blog.csdn.net/zzqtty/article/details/81740104
今日推荐