秒杀业务流程
用户点击商品列表页中的商品,进入商品详情页,如果秒杀成功,则跳转订单详情页。瞬间的并发非常大,系统很可能出现问题,瓶颈在数据库(加缓存,异步化来减轻数据库压力,防止直接穿透到数据库)
秒杀架构设计理念
限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
秒杀系统架构设计思路
将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,最终请求超时。
利用缓存:利用缓存可极大提高系统读写速度。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
测试教程
第一步:
创建Redis测试应用springboot-redis,引入Redis相关依赖,pom.xml配置信息如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
因为这里需要介绍三种方式来解决秒杀超卖的问题,所以引入了三个Redis依赖。
第二步:
修改项目的application.yml配置文件,主要配置应用信息和redis服务信息,配置如下:
server:
port: 8908
spring:
application:
name: springboot-redis
jackson:
# 指定时间格式
date-format: 'yyyy-MM-dd HH:mm:ss'
# 排除结果中属性值是 null 的属性
default-property-inclusion: non_null
redis:
database: 0 #Redis数据库索引(默认为0)
host: 127.0.0.1 #Redis服务器地址
port: 6379 #Redis服务器连接端口
password: # Redis服务器连接密码(默认为空)
timeout: 5000 #连接超时时间(毫秒)
jedis:
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 #连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
第三步:
创建Redisson配置类,代码如下:
package com.test.redis.config;
import io.netty.channel.nio.NioEventLoopGroup;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
@Configuration
public class RedissonConfig {
private String address = "redis://127.0.0.1:6379";
private int connectionMinimumIdleSize = 10;
private int idleConnectionTimeout = 10000;
private int pingTimeout = 1000;
private int connectTimeout = 10000;
private int timeout = 5000;
private int retryAttempts = 3;
private int retryInterval = 1500;
private int reconnectionTimeout = 3000;
private int failedAttempts = 3;
private String password = null;
private int subscriptionsPerConnection = 5;
private String clientName = null;
private int subscriptionConnectionMinimumIdleSize = 1;
private int subscriptionConnectionPoolSize = 50;
private int connectionPoolSize = 64;
private int database = 1;
private boolean dnsMonitoring = false;
private int dnsMonitoringInterval = 5000;
private int thread; //当前处理核数量 * 2
private String codec = "org.redisson.codec.JsonJacksonCodec";
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() throws Exception {
Config config = new Config();
config.useSingleServer().setAddress(address)
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize)
.setDatabase(database)
.setDnsMonitoring(dnsMonitoring)
.setDnsMonitoringInterval(dnsMonitoringInterval)
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
.setSubscriptionsPerConnection(subscriptionsPerConnection)
.setClientName(clientName)
.setFailedAttempts(failedAttempts)
.setRetryAttempts(retryAttempts)
.setRetryInterval(retryInterval)
.setReconnectionTimeout(reconnectionTimeout)
.setTimeout(timeout)
.setConnectTimeout(connectTimeout)
.setIdleConnectionTimeout(idleConnectionTimeout)
.setPingTimeout(pingTimeout)
.setPassword(password);
Codec codec = (Codec) ClassUtils.forName("org.redisson.codec.JsonJacksonCodec", ClassUtils.getDefaultClassLoader()).newInstance();
config.setCodec(codec);
config.setThreads(thread);
config.setEventLoopGroup(new NioEventLoopGroup());
config.setUseLinuxNativeEpoll(false);
return Redisson.create(config);
}
}
第四步:
创建秒杀服务接口,包含秒杀处理方法,代码如下:
package com.test.redis.controller;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@RestController
@RequestMapping("/api")
public class SeckillController {
@Resource(name = "stringRedisTemplate")
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
/**
* 购买成功数量
*/
private AtomicInteger sellCount = new AtomicInteger(0);
/**
* 初始化商品库存数量
* @return
*/
@GetMapping("/initcount")
public String initcount() {
stringRedisTemplate.opsForValue().set("product_count", "5");
sellCount.set(0);
return "初始化库存成功";
}
/**
* 加入事务的减少库存方式
* @return
*/
@GetMapping("/sell1")
public String sell1() {
stringRedisTemplate.setEnableTransactionSupport(true);
List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.watch("product_count");
String product_count = (String) operations.opsForValue().get("product_count");
operations.multi();
operations.opsForValue().get("product_count");
Integer productCount = Integer.parseInt(product_count);
productCount = productCount - 1;
if (productCount < 0) {
return null;
}
operations.opsForValue().set("product_count", productCount.toString());
return operations.exec();
}
});
if (results != null && results.size() > 0) {
return "减少库存成功,共减少" + sellCount.incrementAndGet();
}
return "库存不足";
}
/**
* 直接用jredis加入事务的减少库存方式
* @return
*/
@GetMapping("/sell2")
public String reduceSku3() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
List<Object> result;
Transaction transaction = null;
try {
jedis.watch("product_count");
int product_count = Integer.parseInt(jedis.get("product_count"));
if (product_count > 0) {
transaction = jedis.multi();
transaction.set("product_count", String.valueOf(product_count - 1));
result = transaction.exec();
if (result == null || result.isEmpty()) {
log.error("Transaction error..."); //可能是watch-key被外部修改,或者是数据操作被驳回
//transaction.discard(); //watch-key被外部修改时,discard操作会被自动触发
return "Transaction error...";
}
} else {
return "库存不足";
}
return "减少库存成功,共减少" + sellCount.incrementAndGet();
} catch (Exception e) {
log.error(e.getMessage());
transaction.discard();
return "fail";
}
}
/**
* 通过加锁方式减少库存方式
* @return
*/
@GetMapping("/sell3")
public String sell3() {
RLock rLock = redissonClient.getLock("product_count");
try {
rLock.lock();
Integer product_count = Integer.parseInt(stringRedisTemplate.opsForValue().get("product_count"));
product_count = product_count - 1;
if (product_count < 0) {
return "库存不足";
}
stringRedisTemplate.opsForValue().set("product_count", product_count.toString());
return "减少库存成功,共减少" + sellCount.incrementAndGet();
} finally {
rLock.unlock();
}
}
/**
* 销售成功的数量
* @return
*/
@GetMapping("/sellcount")
public String sellcount() {
return "成功抢到的商品数量:" + sellCount.get();
}
}
代码解析:
先初始化商品库存数量
通过三种防止超卖的方法销售商品
查看商品销售成功的数量
使用spring的redisTemplate执行事务,需要在开启事务后执行一个redis的查询操作(非真实值),原因是:
spring对redis事务的exec()方法返回结果做了处理(把返回值的 OK结果删掉)。
导致在事务中只有set等更新操作时,事务执行失败与成功返回的结果一样。
事务过程中查询redis的值只会在事务执行成功后才放回。而在事务执行过程中只会返回null
第五步:
接下来启动应用与Redis服务,应用启动成功后打开浏览器,访问服务接口顺序如下:
商品初始化接口:http://127.0.0.1:8908/api/initcount
减少库存数量(Redis事务):http://127.0.0.1:8908/api/sell1
减少库存数量(Jredis加事务):http://127.0.0.1:8908/api/sell2
减少库存数量(Redisson锁):http://127.0.0.1:8908/api/sell3
商品销售成功数量:http://127.0.0.1:8908/api/sellcount
接口访问返回数据顺序如下图:
打开Redis桌面管理软件,查看商品库存信息,可以看到商品库存正常,如下图:
以上就是今天为大家讲解的高并发秒杀活动测试,通过三种方法来解决商品超卖问题,通过Jmeter测试软件来模拟1000个请求,最后测试结果商品库存数量也是为0,成功销售数量为5,可以见三种防止超卖的方法都可以。
转载地址: https://mp.weixin.qq.com/s/o_C7JHi78qmKRZuYjdImew