商品订单高并发环境下秒杀案例设计与分析
秒杀代码:
package com.micah.msgcenter.service.impl;
import com.micah.msgcenter.service.StoreService;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
/**
* @Author m.kong
* @Date 2021/7/7 下午4:00
* @Version 1
* @Description
*/
@Service
public class StoreServiceImpl implements StoreService {
@Override
public boolean spike(String userId, String goodsId){
//1、参数判断
if (userId == null || goodsId == null){
System.out.println("秒杀失败");
return false;
}
//2、连接redis
Jedis jedis = new Jedis("www.daxiaobai.site", 6379);
jedis.auth("micah_redis");
//3、拼接key
String storeKey = "store:" + goodsId + ":count";
String userKey = "order:" + goodsId + ":user";
//4、检查库存,库存为空,说明秒杀还未开始
String storeCount = jedis.get(storeKey);
if (storeCount == null){
System.out.println("秒杀还未开始");
jedis.close();
return false;
}
//5、判断用户是否重复秒杀
if (jedis.sismember(userKey, userId)) {
System.out.println("已经参与过秒杀");
jedis.close();
return false;
}
//6、秒杀数量 <= 0 秒杀已经结束
if (Integer.parseInt(jedis.get(storeKey)) <= 0){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7、秒杀过程
//7.1 库存减1
jedis.decr(storeKey);
//7.2 存入订单
jedis.sadd(userKey, userId);
System.out.println("恭喜您,秒杀成功");
jedis.close();
return true;
}
}
控制台输出日志:
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
恭喜您,秒杀成功
秒杀已经结束
恭喜您,秒杀成功
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
秒杀已经结束
redis服务器端指令:
127.0.0.1:6379> ab -n 2000 -c 200 -k -T application/x-www-form-urlencoded http://www.daxiaobai.site:8888/redis/spike?goodsId=0101
[root@VM-4-4-centos ~]# ab -n 2000 -c 200 -k -T application/x-www-form-urlencoded http://www.daxiaobai.site:8888/redis/spike?goodsId=0101
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking www.daxiaobai.site (be patient)
Completed 200 requests
Completed 400 requests
Completed 600 requests
Completed 800 requests
Completed 1000 requests
Completed 1200 requests
Completed 1400 requests
Completed 1600 requests
Completed 1800 requests
Completed 2000 requests
Finished 2000 requests
Server Software:
Server Hostname: www.daxiaobai.site
Server Port: 8888
Document Path: /redis/spike?goodsId=0101
Document Length: 12 bytes
Concurrency Level: 200
Time taken for tests: 5.668 seconds
Complete requests: 2000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 2000
Total transferred: 348000 bytes
HTML transferred: 24000 bytes
Requests per second: 352.89 [#/sec] (mean)
Time per request: 566.756 [ms] (mean)
Time per request: 2.834 [ms] (mean, across all concurrent requests)
Transfer rate: 59.96 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 46 207.9 0 1009
Processing: 44 512 533.1 319 2965
Waiting: 44 512 533.1 319 2965
Total: 50 558 623.0 319 3137
Percentage of the requests served within a certain time (ms)
50% 319
66% 363
75% 368
80% 417
90% 1570
95% 2330
98% 2680
99% 2816
100% 3137 (longest request)
[root@VM-4-4-centos ~]#
2 超卖问题
通过乐观锁进行解决
3 连接超时问题
节省每次连接redis服务带来的消耗,把连接好的实例反复利用。
通过参数管理连接的行为
链接池参数
MaxTotal
:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
maxIdle
:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
MaxWaitMillis
:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
testOnBorrow
:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
package com.micah.msgcenter.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @Author m.kong
* @Date 2021/7/8 上午11:22
* @Version 1
* @Description
*/
public class JedisPoolUtil {
private static JedisPool jedisPool;
static {
try {
// 读取配置文件
Properties pro = new Properties();
InputStream is = JedisPoolUtil.class.getClassLoader().getResourceAsStream("jedis_config.properties");
pro.load(is);
// 创建jedis连接池配置对象
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));
jedisPoolConfig.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));
// 使用jedis连接池配置对象创建jedis连接池配置对象
jedisPool = new JedisPool(jedisPoolConfig, pro.getProperty("host"), Integer.parseInt(pro.getProperty("port")), Integer.parseInt(pro.getProperty("timeout")), pro.getProperty("auth"));
System.out.println("连接成功");
} catch (IOException e) {
System.out.println("连接失败");
e.printStackTrace();
}
}
/**
* 获取jedis连接
*/
public static Jedis getJedis() {
return jedisPool.getResource();
}
/**
* 释放资源
*/
public static void close(final Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
public static void main(String[] args) {
JedisPoolUtil.getJedis();
Jedis jedis = JedisPoolUtil.jedisPool.getResource();
System.out.println(jedis.get("store:0101:count"));
}
}
4、库存遗留问题
使用嵌入lua脚本来解决这一问题。