主要思想還是限流。秒殺商品有開始時間和結束時間,庫存可以看成是token,所以本質上還是一個基於令牌桶限流的變種場景。每個限流的單位時間不是1秒,而是秒殺活動持續的時間長度,庫存看作是的單位時間加入到令牌桶的令牌數。和令牌桶唯一的區別是秒殺只有一個單位時間內有令牌。
import redis.clients.jedis.Jedis;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class Kill {
private Jedis client;
private static final String START_TIME_KEY = "start_time";
private static final AtomicInteger count = new AtomicInteger();
private static ConcurrentHashMap<Long, Goods> goodsMap = new ConcurrentHashMap<>();
public Kill() {
client = new Jedis("192.168.31.206");
}
// 準備,參與秒殺的商品入redis
private void addGoods(Goods goods) {
goodsMap.put(goods.getId(), goods);
client.hset(START_TIME_KEY, String.valueOf(goods.getId()), String.valueOf(goods.getStartTime().toEpochSecond(ZoneOffset.ofHours(8))));
client.set(goods.getId() + ":" + goods.getStartTime().toEpochSecond(ZoneOffset.ofHours(8)), String.valueOf(goods.getStock()));
}
/**
* @param goodsId
* @param permits
* @return 1爲活動未開始,2爲正常購買,3爲庫存不足,4爲賣光了,5爲活動已結束
*/
private Integer buy(long goodsId, int permits) {
assert permits > 0;
Long startTime = Long.valueOf(client.hget(START_TIME_KEY, goodsId + ""));
long current = System.currentTimeMillis() / 1000;
long difference = current - startTime;
long duration = goodsMap.get(goodsId).getDuration().toMillis() / 1000;
long time = (long) (startTime + Math.floor(difference / (duration * 1.0)) * duration);
String script = "local count=redis.call('get',KEYS[1])\n" +
"if type(count) == 'boolean' then\n" +
" return -1;\n" +
"end\n" +
"count=tonumber(count)\n" +
"if count-ARGV[1]>=0 then\n" +
" redis.call('decrBy',KEYS[1],ARGV[1])\n" +
" return 1\n" +
"end\n" +
"return 0";
Long count = (Long) client.eval(script, Collections.singletonList(goodsId + ":" + time), Collections.singletonList(permits + ""));
if (count < 0) {
if (current < startTime) {
return 1;
} else {
return 5;
}
} else if (count == 0) {
if (permits > 1) {
return 3;
} else {
return 4;
}
} else {
return 2;
}
}
public void refund(long goodsId, int permits) {
Long startTime = Long.valueOf(client.hget(START_TIME_KEY, goodsId + ""));
client.incrBy(goodsId + ":" + startTime, permits);
}
// 模擬秒殺過程
public static void main(String[] args) throws InterruptedException {
Kill kill1 = new Kill();
kill1.addGoods(new Goods(123L, 100, LocalDateTime.now(ZoneOffset.ofHours(8)), Duration.ofDays(1L)));
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int j = 0; j < 100; j++) {
Random random = new Random();
new Thread(() -> {
Kill kill = new Kill();
int permits = 1 + random.nextInt(9);
Integer buy = kill.buy(123L, permits);
if (buy == 2) {
count.addAndGet(permits);
}
Arrays.stream(Flag.values()).filter(flag -> flag.value.equals(buy)).findFirst().ifPresent(flag -> System.out.println(flag.desc));
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
}
enum Flag {
_1(1, "活動未開始"), _2(2, "正常買"), _3(3, "庫存不足"), _4(4, "賣光了"), _5(5, "活動已結束");
private Integer value;
private String desc;
Flag(Integer value, String desc) {
this.value = value;
this.desc = desc;
}
}
static class Goods {
private Long id;
private Integer stock;
private LocalDateTime startTime;
private Duration duration;
public Goods(Long id, Integer stock, LocalDateTime startTime, Duration duration) {
this.id = id;
this.stock = stock;
this.startTime = startTime;
this.duration = duration;
}
public Long getId() {
return id;
}
public Integer getStock() {
return stock;
}
public LocalDateTime getStartTime() {
return startTime;
}
public Duration getDuration() {
return duration;
}
}
}