高併發搶紅包案列以及使用鎖,版本號,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,安外掛,匯入
下一篇介紹下解決