需求设计
- 商品种类数确定,每种商品的库存数确定
- 顾客上万
- 需要输出秒杀成功的客户信息
待解决的问题
- 模拟上万的买家
- 设置秒杀的开始,结束时间节点
- 保证商品的原子性
- 保证商品卖出量小于等于库存量
解决办法
- 线程池
- 线程沉睡
- 采用Redis监听和事务处理
- 多次获取商品的数量与原始数量对比
代码
redis配置类
public class RedisUtil{
// slf4j日志
protected static Logger logger = LoggerFactory.getLogger(RedisUtil.class);
// 主机和端口
private static final String hostname = "127.0.0.1";
private static final int port = 6379;
// jedisPool
public static JedisPool jedisPool = null;
private RedisUtil(){
}
/**
* 在多线程环境同步初始化jedispool
* synchronized
*/
public static synchronized void poolInit(){
if(jedisPool == null){
initialPool();
}
}
/**
* 初始化JedisPool
*/
private static void initialPool(){
if(jedisPool == null){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 指定连接池中最大的空闲连接数
jedisPoolConfig.setMaxIdle(100);
// 连接池创建的最大连接数
jedisPoolConfig.setMaxTotal(500);
// 设置创建连接的超时时间
jedisPoolConfig.setMaxWaitMillis(1000 * 50);
// 表示从连接池中获取连接时,先测试连接是否可用
jedisPoolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(jedisPoolConfig, hostname, port);
}
}
/**
* 释放jedis资源
* @param jedis
*/
public static void returnResource(Jedis jedis) {
if (jedis != null && jedisPool != null) {
jedisPool.returnResource(jedis);
jedis.close();
}
}
}
redis多线程类(核心)
代码框架
// 通过继承Thread类,重写run方法实现多线程
public class RedisThread extends Thread{
// 注入jedispool
private JedisPool jedisPool;
// 需要购买的商品
private String pro;
// 记录所有线程抢到的商品数量并初始化
private static int count = 0;
// 记录秒杀开始的时间
private static long startTime = 0;
// 记录所有线程秒杀过程总共消耗的时间
private static long wasteTime = 0;
@Override
public void run() {
// 实现监听商品,购买商品,更新库存,提交事务,打印信息等业务逻辑
}
// 定时器方法
private long clock(){
}
// 打印成功抢购信息
private synchronized void printLog(String string){
}
}
具体实现
// 构造方法
public RedisThread(JedisPool jedisPool, String pro) {
this.jedisPool = jedisPool;
this.pro = pro;
}
// 定时器方法
private long clock(){
String prepareClock = "2020-06-28 13:56:00";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;
try {
date = simpleDateFormat.parse(prepareClock);
} catch (ParseException e) {
e.printStackTrace();
}
long time = date.getTime();
return time;
}
// 线程执行函数
@Override
public void run() {
// 取当前时间
long currentTimeMillis = System.currentTimeMillis();
// 判断秒杀时间是否已到活动开始时间
long millis = clock() - currentTimeMillis;
if (millis > 0) {
// 表示还没到时间,还需要使线程等待
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while(true){
// 只要还有没卖出的货品pre,线程就可以一直循环,一个客户可以秒杀同时秒杀多个相同的商品
try {
// 让线程先等待0.1秒
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
// 处理业务
try{
// 从redis里面获取商品的数据
int proNum = Integer.parseInt(jedis.get(pro));
// 用于记录提交事务的结果
List<Object> result = null;
if(proNum > 0){
// 监听
jedis.watch(pro);
// 再次判断数量是否被其他线程改变
int proNumAfter = Integer.parseInt(jedis.get(pro));
if(proNumAfter < proNum){
// 说明其他线程已经改变了该值
// 放弃监视
jedis.unwatch();
}else{
// 开始Redis事务
Transaction transaction = jedis.multi();
// 购买商品,然后更改redis中的库存数据
transaction.set(pro, String.valueOf(proNum - 1));
// 提交事务
result = transaction.exec();
}
// 如果result为空说明事务没有提交,说明监听的商品被正在别的线程操作,
// 本线程无法购买商品,需要排队,而不能修改商品的数量
if(result == null || result.isEmpty()){
System.out.println(Thread.currentThread().getName() + "\t正在排队抢购\t" + pro + "...");
}else{
// 说明秒杀成功
// count为静态变量,每个对象的公共区域,用于计算卖出的总数
count++;
switch (count){
case 1:
startTime = System.currentTimeMillis();
break;
case 50:
wasteTime = System.currentTimeMillis() - startTime;
System.out.println("===================" + wasteTime);
break;
default:
break;
}
String str = Thread.currentThread().getName() + "\t抢购成功,商品名为:\t" + pro + "\t抢购时间:"
+ new Timestamp(System.currentTimeMillis());
System.out.println(str);
// 把抢购成功的顾客信息打印出去,并记录日志
printLog(str);
}
}else{
// 库存为0时
System.out.println(pro + "已售罄,库存为0");
break;
}
}catch (Exception e){
e.printStackTrace();
RedisUtil.returnResource(jedis);
}finally {
RedisUtil.returnResource(jedis);
}
}
}
private synchronized void printLog(String string){
// 反射获取项目根目录
Class clazz = RedisMs.class;
URL url = clazz.getResource("/");
String path = url.toString();
// 去掉file://
path = path.substring(6);
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(path + "/result.txt", true));
bw.write(string);
bw.newLine();
bw.flush();
}catch(IOException e){
e.printStackTrace();
}finally {
try {
bw.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
主函数
public class RedisMs {
public static void main(String[] args) {
// 货物清单
String[] arr = { "iphone", "oppo", "meizu", "xiaomi", "huawei" };
// 初始化池
RedisUtil.poolInit();
// 单例获取池对象
JedisPool jedisPool = RedisUtil.jedisPool;
// 将货物分配数值,并存入redis中
assigning(arr, 10, jedisPool);
// 模拟1万个客户秒杀抢购
robBuying(arr, 10000, jedisPool);
}
/**
* 为Redis数据库中的商品赋值
* @param arr String 抢购商品数组
* @param num int 商品库存
*/
private static void assigning(String[] arr, int num, JedisPool jedisPool) {
// 获得redis的连接
Jedis jedis = jedisPool.getResource();
// 对每个商品赋值
// 值用string存储
for (int i = 0; i < arr.length; i++) {
jedis.set(arr[i], String.valueOf(num));
}
}
private static void robBuying(String[] arr, int threadNum, JedisPool jedisPool){
// 开启线程池,模拟上万个顾客
ExecutorService robBuyThreadPool = Executors.newFixedThreadPool(threadNum);
// 声明随机种子,模拟随机秒杀,对货品的随机
Random random = new Random();
// 对每个客户进行循环,并提交事务
for(int i = 0; i < threadNum; i++){
// 为线程随机传递需要抢购的商品
int index = random.nextInt(5);
// 为每一个用户新建一个线程
RedisThread redisThread = new RedisThread(jedisPool, arr[index]);
robBuyThreadPool.submit(redisThread);
}
// 活动结束,关闭线程池
fixedThreadPool.shutdown();
}
}
运行结果
运行结果如图