1 Database design
T_secKILL_goods
CREATE TABLE `t_seckill_goods` (
`id` bigint NOT NULL,
`goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_num` int DEFAULT NULL,
`price` decimal(10.2) DEFAULT NULL.PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Copy the code
Table of seckill_order
CREATE TABLE `t_seckill_order` (
`id` bigint NOT NULL,
`seckill_goods_id` bigint DEFAULT NULL,
`user_id` bigint DEFAULT NULL,
`seckill_goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_goods_price` decimal(10.2) DEFAULT NULL.PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Copy the code
Initial data:
One item, inventory 10
2 Architecture
The structure of the demo code is: Springboot Redis Redisson Sentinel, which uses custom exceptions, global exception interception, unified return object and so on
3 use jmeter
3.1 Modifying the Configuration
Download JMeter and modify the configuration file jmeter.properties
Language = zh_CN # change language to Chinese sampleresult. Default. The encoding = utf-8 # the default encoding to utf-8Copy the code
3.2 Adding a Test Thread Group
3.2.1 Setting parameters From the CSV file -> Add Configuration Components ->CSV data set config
3.2.2 Adding a Sampler -> HTTP Request
3.2.3 Adding Configuration Components -> HTTP Header Manager
3.2.4 Adding a Listener > View the Result tree
Kill code in 4 seconds
4.1 version 1
@Service
public class TSeckillGoodsServiceImpl extends ServiceImpl<TSeckillGoodsMapper.TSeckillGoods> implements TSeckillGoodsService {
@Autowired
private TSeckillGoodsMapper seckillGoodsMapper;
@Autowired
private TSeckillOrderMapper seckillOrderMapper;
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 Determine whether the product exists
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202."The goods do not exist.");
}
//2 Determine whether the user has already purchased the service
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have already purchased this product.");
}
//3 Inventory reduction operation
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 Insert order
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("Send mq, tell the user to pay as soon as possible."); }}else {
throw new BusinessException(203."Stock is low, please snap up other items."); }}}Copy the code
Output:
Conclusion:
The commodity list is not oversold, but there are many orders and repeated purchases. Therefore, the problem occurs in the following positions:
//2 Determine whether the user has already purchased the service
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have already purchased this product.");
}
//3 Inventory reduction operation
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 Insert order
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("Send mq, tell the user to pay as soon as possible."); }}else {
throw new BusinessException(203."Stock is low, please snap up other items.");
}
Copy the code
Multiple threads simultaneously determine that the current user has not purchased an item, and then the number >0 inserts the order
4.2 version 2
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 Determine whether the product exists
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202."The goods do not exist.");
}
//2 Determine whether the user has already purchased the service
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have purchased the item or the item does not exist.");
}
//3 Inventory reduction operation
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
if (i > 0) {
//4 Insert order
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("Send mq, tell the user to pay as soon as possible."); }}else {
throw new BusinessException(203."Stock is low, please snap up other items."); }}Copy the code
Output:
Conclusion:
Modified the third step, after the multi-step operation to a SQL atomic operation, no oversold phenomenon; However, if there is a repeat purchase problem, the main reason is that the second step, “determine if the user has already purchased”, has thread safety issues
4.3 version 3
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 Determine whether the product exists
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202."The goods do not exist.");
}
//2 Determine whether the user has already purchased the service
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have purchased the item or the item does not exist.");
}
//3 Inventory reduction operation
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
//4 Insert order
if (i > 0) {
// Simulate volatile singleton design pattern, double check; I'm not going to bother to change the redundant code here
boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have purchased the item or the item does not exist.");
}
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("Send mq, tell the user to pay as soon as possible."); }}else {
throw new BusinessException(203."Stock is low, please snap up other items."); }}Copy the code
Output:
In order to increase the error rate, I increased the inventory capacity to 20
Conclusion:
This version changes in step 4 to simulate the double check of the volatile singleton design pattern; It can be seen that the probability of repeat purchase has decreased a lot, but it still exists, so it cannot completely solve the problem
4.4 the version 4
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 Determine whether the product exists
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202."The goods do not exist.");
}
final String key = "lock:" + seckillDto.getUserId() + "-" + seckillDto.getGoodsId();
RLock lock = redissonClient.getLock(key);
try {
// The default redis expiration time is 30s
lock.lock();
//2 Determine whether the user has already purchased the service
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201."You have purchased the item or the item does not exist.");
}
//3 Inventory reduction operation
if (seckillGoodsMapper.updateInventory(seckillDto.getGoodsId()) > 0) {
//4 Insert order
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("Send mq, tell the user to pay as soon as possible."); }}else {
throw new BusinessException(203."Stock is low, please snap up other items."); }}finally{ lock.unlock(); }}Copy the code
Output:
Conclusion:
This version, the use of the Redisson framework to do distributed locking, testing several times without problems;
-
Why not apply Redis for distributed locking?
The main reason is that Redis does not have intelligent handling of expiration time, which still causes thread safety and even deadlock problems. Redisson doesn’t
-
Doesn’t Redisson have a problem?
In redis master-slave architecture, if the master fails to synchronize data to Salve, there will still be problems, but the probability is very small, so Redisson can only guarantee AP, not consistency; Using ZooKeeper solves the data consistency problem, but avaliability (availability) is poor
Supplement:
Why not limit the scope of the lock to step 2? Isn’t the problem just the insertion of duplicate data?
5 sentinel current limit
5.1 introduced
If we look back at the impact time of 10,000 requests per second, we can see that the average impact time is over 200 seconds. If it was in production, it would be a crash for the user, and even the front end would report a timeout.
So in such a concurrent, we will inevitably choose distributed deployment, the cluster of the cluster, the database table, hot and cold data separation and so on a series of operations. Sentinel is introduced here to limit traffic, reduce server pressure and improve user experience
5.2 speaking
Pom.xml is imported into Alibaba Sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.6. RELEASE</version>
</dependency>
Copy the code
Add @resource annotation to Controller
@RestController
@RequestMapping("/seckill")
public class TSeckillGoodsController {
@Autowired
private TSeckillGoodsService seckillGoodsService;
@PostMapping("goods")
@SentinelResource(value = "seckill",blockHandler = "seckillHandler")
public String goods(@RequestBody SeckillDto seckillDto) {
seckillGoodsService.seckill(seckillDto);
return "Order successful, please pay as soon as possible.";
}
public static String seckillHandler(SeckillDto seckillDto, BlockException e) {
return "System busy, please try again later"; }}Copy the code
The Sentinel Dashboard has a flow control rule, which is set to allow 10 QPS per second
5.3 the results
Still 10,000 threads per second, the average response time is less than 1 second, which feels ok. You can adjust your QPS threshold and feel it
You can also look at the view tree of Jemeter and see that our own stream limiting exception is in effect
Show me the source code