简单来说就是当大量数据来访问数据库的时候,可能导致数据不一致。如下:
发一个2000元的大红包,总共2000个小红包,每个一元,但是有30000个人去抢,红包少一个就减一,插入抢红包用户信息,结果看图:
stock表示余留的红包数,结果是负一
看见那个2001,居然有2001个人抢到了红包,这就是问题所在了。
接下来我会给出整个项目,和讲解。
红包表:
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元');
其实大家的英文都看得懂吧;
这采用了注解开发的模式,当然用xml配置是一样的,你也可以用springboot,但是原理都一样的
config:配置文件
dao:sql语句
pojo:对象
service:具体的逻辑
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
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
update T_RED_PACKET set stock = stock -1 where id =#{id}
UserRedPacktMapping.xml
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})
对象: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,安插件,导入
下一篇介绍下解决