1、搭建
项目使用springboot 2.3.0.RELEASE、mysql 5.7.27 进行构建,压测工具使用的是JMeter,后续会用到redis,开发工具为IDEA
1.1 数据库
在mysql中建立一个red_packet的数据库,有两个表,分别如下。
CREATE TABLE `t_red_packet` (
`id` int(12) NOT NULL AUTO_INCREMENT,
`user_id` int(12) DEFAULT NULL,
`amount` decimal(16,2) DEFAULT NULL,
`send_date` timestamp NULL DEFAULT NULL,
`total` int(12) DEFAULT NULL,
`unit_amount` decimal(12,0) DEFAULT NULL,
`stock` int(12) DEFAULT NULL,
`version` int(12) unsigned DEFAULT NULL,
`note` varchar(256) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_red_packet` (
`id` int(12) NOT NULL AUTO_INCREMENT,
`red_packet_id` int(12) DEFAULT NULL,
`user_id` int(12) DEFAULT NULL,
`amount` decimal(16,2) DEFAULT NULL,
`grab_time` timestamp NULL DEFAULT NULL,
`note` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入数据
INSERT INTO `t_red_packet` VALUES (1, 1, 500.00, '2020-05-22 11:10:29', 50, 10, 50, 50, '');
1.2 创建
使用IDEA创建一个名为grap-pocket
的SpringBoot
项目,导入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
修改application.yml
文件
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/red_pocket?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 50
min-idle: 1
max-active: 200
max-wait: 600000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
max-wait-thread-count: 30000
mybatis:
mapper-locations:
- classpath:mapper/*.xml
type-aliases-package: cn.amoqi.grap.pocket.entity
configuration:
map-underscore-to-camel-case: true
配置启动类的Mapper扫描路径
@MapperScan("cn.amoqi.grap.pocket.dao")
创建controller、dao、server、entity文件夹
1.3 Entity
package cn.amoqi.grap.pocket.entity;
import lombok.Data;
import java.io.Serializable;
import java.sql.Timestamp;
@Data
public class RedPacket implements Serializable {
private Long id;
private Long userId;
private Double amount;
private Timestamp sendDate;
private Integer total;
private Double unitAmount;
private Integer stock;
private Integer version;
private String note;
}
package cn.amoqi.grap.pocket.entity;
import lombok.Data;
import java.io.Serializable;
import java.sql.Timestamp;
@Data
public class UserRedPacket implements Serializable {
private Long id;
private Long redPacketId;
private Long userId;
private Double amount;
private Timestamp grabTime;
private String note;
}
1.4 Dao
package cn.amoqi.grap.pocket.dao;
import cn.amoqi.grap.pocket.entity.RedPacket;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface RedPacketDao {
/**
* 获取红包信息
* @param id 红包 id
* @return 红包具体信息
*/
@Select("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}")
public RedPacket getRedPacket(Long id);
/**
* 扣减抢红包数
* @param id 红包id
* @return 更新记录条数
*/
@Update("update t_red_packet set stock = stock-1 where id = #{id}")
public int decreaseRedPacket(Long id);
}
package cn.amoqi.grap.pocket.dao;
import cn.amoqi.grap.pocket.entity.UserRedPacket;
import org.apache.ibatis.annotations.Insert;
public interface UserRedPacketDao {
@Insert("insert into T_USER_RED_PACKET(red_packet_id,user_id,amount,grab_time,note) values(#{redPacketId},#{userId},#{amount},now(),#{note})")
public int grapRedPacket(UserRedPacket userRedPacket);
}
1.5 Service
package cn.amoqi.grap.pocket.service;
import cn.amoqi.grap.pocket.entity.RedPacket;
import java.util.List;
public interface RedPacketService {
List<RedPacket> getRedPacketList();
public RedPacket getRedPacket(Long id);
public int decreaseRedPacket(Long id);
}
1.6 Impl
package cn.amoqi.grap.pocket.service;
import cn.amoqi.grap.pocket.dao.RedPacketDao;
import cn.amoqi.grap.pocket.entity.RedPacket;
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 java.util.List;
@Service
public class RedpacketServiceImpl implements RedPacketService{
@Autowired
private RedPacketDao redPacketDao;
@Override
public List<RedPacket> getRedPacketList(){
return redPacketDao.getList();
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public RedPacket getRedPacket(Long id) {
return redPacketDao.getRedPacket(id);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int decreaseRedPacket(Long id) {
return redPacketDao.decreaseRedPacket(id);
}
}
package cn.amoqi.grap.pocket.service;
import cn.amoqi.grap.pocket.dao.RedPacketDao;
import cn.amoqi.grap.pocket.dao.UserRedPacketDao;
import cn.amoqi.grap.pocket.entity.RedPacket;
import cn.amoqi.grap.pocket.entity.UserRedPacket;
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 java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class UserRedPacketServiceImpl implements UserRedPacketService{
private static final int FAILED = 0;
@Autowired
private UserRedPacketDao userRedPacketDao;
@Autowired
private RedPacketDao redPacketDao;
private volatile AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 保存抢红包的信息
* @param redPacketId 红包编号
* @param userId 抢红包的用户编号
* @return 影响记录数目
*/
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId){
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
//当前小红包库存大于0
if(redPacket.getStock() > 0){
redPacketDao.decreaseRedPacket(redPacketId);
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包 "+redPacketId);
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}
return FAILED;
}
}
1.7 Controller
package cn.amoqi.grap.pocket.controller;
import cn.amoqi.grap.pocket.service.UserRedPacketService;
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 java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
@Autowired
private UserRedPacketService userRedPacketService;
@RequestMapping(value = "/grapRedPacket")
@ResponseBody
public Map<String,Object> grapRedPacket(Long redPacketId, Long userId){
//抢红包
int result = userRedPacketService.grapRedPacket(redPacketId,userId);
Map<String,Object> retMap = new HashMap<>();
boolean flag = result>0;
retMap.put("success",flag);
retMap.put("message",flag?"抢红包":"抢红包失败");
return retMap;
}
@RequestMapping("/1")
public String test(){
return "grap";
}
}
1.8 测试
使用Jmeter创建线程组,线程数为400,并在线程组下面创建HTTP请求,请求地址如下,其他的设置默认即可
http://localhost:8080/userRedPacket/grapRedPacket?redPacketId=1&userId=1
多测试几次(还原t_red_packet
字段stock为50,清空t_user_red_packet
),会发现 t_user_red_packet
表中的stock
字段值可能出现负数,出现了超买问题
2、悲观锁 for update
可以使用for update
进行锁定表的行
在 RedPacketDao
查询库存接口getRedPacket
加入 for update
@Select("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")
public RedPacket getRedPacket(Long id);
使用Jmeter测试后发现库存不会存在超卖的问题
该方法属于悲观锁,加入for update 后会对行进行加锁(因为这里使用主键查询,加锁后可能会引发其他查询的堵塞),意味着高并发情况下当一个事务有了这个更新锁才能往下执行,其他线程如果要更新这条记录就需要等待,这样不会出现超发现象。
3、乐观锁
3.1 使用版本号处理
恢复 RedPacketDao
查询库存接口getRedPacket
的方法,不加入for update
在 RedPacketDao
中加入方法
//使用版本号的方式进行
@Update("update t_red_packet set stock = stock-1, version=version+1 where id = #{id} and version = #{version}")
public int decreaseRedPacketForVersion(Long id,Integer version);
修改 UserRedPacketServiceImpl
的grapRedPacket
方法
/**
* 保存抢红包的信息
* @param redPacketId 红包编号
* @param userId 抢红包的用户编号
* @return 影响记录数目
*/
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId){
//使用for update时候会在读库存的时候锁住表,只能其他线程提交或者回滚事务释放锁
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);
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}
return FAILED;
}
因为一些请求返回失败,会剩余许多库存
3.2 重入机制保证库存消耗
使用时间戳进行重入,修改 UserRedPacketServiceImpl
的grapRedPacket
方法
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId){
long start = System.currentTimeMillis();
while (true){
//获取循环当前时间
long end = System.currentTimeMillis();
if(end -start>200){
return FAILED;
}
//使用for update时候会在读库存的时候锁住表,只能其他线程提交或者回滚事务释放锁
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
//当前小红包库存大于0
if(redPacket.getStock() > 0){
//如果传入的线程保存version旧值给SQL判断,是否有其他线程修改过数据
int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
if(update == 0){
continue;
}
//生成抢红包的信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包 "+redPacketId);
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}else{
return FAILED;
}
}
}
从结果上看没有出现超发现象,而且库存会清空。也可以考虑到重试次数
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId){
long start = System.currentTimeMillis();
for(int i = 0;i<=3;i++){
//使用for update时候会在读库存的时候锁住表,只能其他线程提交或者回滚事务释放锁
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
//当前小红包库存大于0
if(redPacket.getStock() > 0){
//如果传入的线程保存version旧值给SQL判断,是否有其他线程修改过数据
int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
if(update == 0){
continue;
}
//生成抢红包的信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包 "+redPacketId);
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}else{
return FAILED;
}
}
return FAILED;
}