背景
双11,618等各大电商节越来越多,最吸引人的莫不是秒杀商品的活动。那秒杀活动的背后,又是用什么技术实现的呢?是不是很好奇?另外不懂秒杀的,会被面试官虐吗?不要好奇,不要担心,小编给你介绍一个小案例,开拓下思维。
环境
- jdk1.8+
- spring boot
- redis
思路
- 模拟用户发起秒杀操作,首先判断库存是否足够,如果库存为0,则秒杀结束
2.如果库存足够,则判断用户是否拿到该商品的redis分布式锁,如果拿到锁,则进入下单业务逻辑,如果没拿到锁,则30秒后再次进入秒杀逻辑 3. 进入下单逻辑后,还需要再次判断下库存,库存为0,则秒杀结束,反之生成订单。(这里是为了进一步防止超卖发生)
搭建
生成springboot项目,pom引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.yml添加redis相关配置
各位可以根据实际情况来配置
server:
port: 6004
spring:
application:
name: spring-boot-ms
# Redis配置
redis:
host: 你自己的redis的ip地址
# Redis服务器连接端口
port: 你自己的redis的端口
# Redis服务器连接密码(默认为空)
password: liyajie@2021
timeout: 30000
# 连接池最大连接数(使用负值表示没有限制)
maxTotal: 50
# 连接池中的最大空闲连接
maxIdle: 10
numTestsPerEvictionRun: 1024
timeBetweenEvictionRunsMillis: 30000
minEvictableIdleTimeMillis: 1800000
softMinEvictableIdleTimeMillis: 10000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
maxWaitMillis: 1500
testOnBorrow: true
testWhileIdle: true
blockWhenExhausted: false
JmxEnabled: true
定义redis配置类
@Configuration
@PropertySource("classpath:application.yml")
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.timeout}")
private int timeout;
@Value("${redis.password}")
private String password;
@Value("${redis.maxIdle}")
private int maxIdle;
@Value("${redis.maxTotal}")
private int maxTotal;
@Value("${redis.maxWaitMillis}")
private int maxWaitMillis;
@Value("${redis.blockWhenExhausted}")
private Boolean blockWhenExhausted;
@Value("${redis.JmxEnabled}")
private Boolean JmxEnabled;
@Bean
public JedisPool jedisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
// 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
// 是否启用pool的jmx管理功能, 默认true
jedisPoolConfig.setJmxEnabled(JmxEnabled);
jedisPoolConfig.setTestOnBorrow(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
return jedisPool;
}
}
定义测试controller类
@RestController
@Slf4j
public class MsController {
@Autowired
private RedisUtils redisUtils;
@Autowired
MsService msService;
//总库存
private long nKuCuen = 0;
//商品key名字
private String shangpingKey = "computer_key";
//获取锁的超时时间 秒
private int timeout = 30 * 1000;
/**
* 设置分布式锁
* @param
* @return boolean
* @author liyajie
* @createTime 2021/12/16 14:52
**/
@GetMapping("/setnx/{key}/{val}")
public boolean setnx(@PathVariable String key, @PathVariable String val) {
return redisUtils.setnx(key, val);
}
/**
* 删除分布式锁
* @param
* @return int
* @author liyajie
* @createTime 2021/12/16 14:52
**/
@GetMapping("/delnx/{key}/{val}")
public int delnx(@PathVariable String key, @PathVariable String val) {
return redisUtils.delnx(key, val);
}
/**
* 抢单
* @param
* @return List<String>
* @author liyajie
* @createTime 2021/12/16 14:54
**/
@GetMapping("/qiangdan")
public List<String> qiangdan() {
//抢到商品的用户
List<String> shopUsers = new ArrayList<>();
//构造很多用户
List<String> users = Collections.synchronizedList(new ArrayList<String>());
IntStream.range(0, 100000).parallel().forEach(b -> {
users.add("神器-" + b);
});
//初始化库存
nKuCuen = 10;
//模拟开抢
users.parallelStream().forEach(b -> {
String shopUser = this.qiang(b);
if (!StringUtils.isEmpty(shopUser)) {
shopUsers.add(shopUser);
}
});
return shopUsers;
}
private String qiang(String b) {
//用户开抢时间
long startTime = System.currentTimeMillis();
//未抢到的情况下,30秒内继续获取锁
while ((startTime + timeout) >= System.currentTimeMillis()) {
//商品是否剩余
if (nKuCuen <= 0) {
break;
}
if (redisUtils.setnx(shangpingKey, b)) {
//用户b拿到锁
log.info("用户{}拿到锁...", b);
try {
//商品是否剩余
if (nKuCuen <= 0) {
break;
}
//模拟生成订单耗时操作,方便查看:神器-50 多次获取锁记录
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//抢购成功,商品递减,记录用户
nKuCuen -= 1;
//抢单成功跳出
log.info("用户{}抢单成功跳出...所剩库存:{}", b, nKuCuen);
return b + "抢单成功,所剩库存:" + nKuCuen;
} finally {
log.info("用户{}释放锁...", b);
//释放锁
redisUtils.delnx(shangpingKey, b);
}
} else {
log.info("用户{}等待获取锁...", b);
// 用户b没拿到锁,在超时范围内继续请求锁,不需要处理
/*if (b.equals("神器-50") || b.equals("神器-69")) {
log.info("用户{}等待获取锁...", b);
}*/
}
}
return "";
}
}
测试
总结
这个秒杀小案例主要用到的知识点就是redis的分布式锁的唯一性,希望大家温故知新。
需要源码的可以关注公众号【温故知新之java】,更多干活与你分享。