1. 程式人生 > >高併發搶紅包案列以及使用鎖,版本號,redis快取解決,專案可執行,詳細註釋(一)

高併發搶紅包案列以及使用鎖,版本號,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:總紅包金額大小

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,安外掛,匯入

下一篇介紹下解決