秒杀系统设计架构与实现
最近做了一個點餐的平臺,其中涉及到一個很重要的問題,活動期間的秒殺系統(tǒng)的實現(xiàn)。
搶購/秒殺是如今很常見的一個應(yīng)用場景,是高并發(fā)編程的一個挑戰(zhàn),在網(wǎng)上也找了一些資料,大部分都是理論,關(guān)于java的實現(xiàn)也是很少,就算有也是很簡單的demo,為此,決定將此次實現(xiàn)的秒殺系統(tǒng)整理一番,發(fā)布出來。
架構(gòu)思路
Question1: 由于要承受高并發(fā),mysql在高并發(fā)情況下的性能下降尤其嚴(yán)重,下圖為Mysql性能瓶頸測試。
而且硬盤持久化的io操作將耗費大量資源。所以決定采用基于內(nèi)存操作的redis,redis的密集型io
Question2: 秒殺系統(tǒng)必然是一個集群系統(tǒng),在硬件不提升的情況下利用nginx做負(fù)載均衡也是不錯的選擇。
實現(xiàn)難點
1. 超買超賣問題的解決。
2.?訂單持久化,多線程將訂單信息寫入數(shù)據(jù)庫
解決方案
1. 采用redis的分布式樂觀鎖,解決高并發(fā)下的超買超賣問題.
2. 使用countDownLatch作為計數(shù)器,將數(shù)據(jù)四線程寫入數(shù)據(jù)庫,訂單的持久化過程在我的機器上效率提升了1000倍。
進階方案
1.訪問量還是大。系統(tǒng)還是撐不住。
2.防止用戶刷新頁面導(dǎo)致重復(fù)提交。
3.腳本攻擊
解決思路:
1.訪問量還是過大的話,要看性能瓶頸在哪里,一般來說首先撐不住的是tomcat,考慮優(yōu)化tomcat,單個tomcat經(jīng)過實踐并發(fā)量撐住1000是沒有問題的。先搭建tomcat集群,如果瓶頸出現(xiàn)在redis上的話考慮集群redis,這時候消息隊列也是必須的,至于采用哪種消息隊列框架還是根據(jù)實際情況。
2.問題2和問題3其實屬于同一個問題。這個問題其實屬于網(wǎng)絡(luò)問題的范疇,和我們的秒殺系統(tǒng)不在一個層面上。因此不應(yīng)該由我們來解決。很多交換機都有防止一個源IP發(fā)起過多請求的功能。開源軟件也有不少能實現(xiàn)這點。如linux上的TC可以控制。流行的Web服務(wù)器Nginx(它也可以看做是一個七層軟交換機)也可以通過配置做到這一點。一個IP,一秒鐘我就允許你訪問我2次,其他軟件包直接給你丟了,你還能壓垮我嗎?
交換機也不行了呢?
可能你們的客戶并發(fā)訪問量實在太大了,交換機都撐不住了。 這也有辦法。我們可以用多個交換機為我們的秒殺系統(tǒng)服務(wù)。 原理就是DNS可以對一個域名返回多個IP,并且對不同的源IP,同一個域名返回不同的IP。如網(wǎng)通用戶訪問,就返回一個網(wǎng)通機房的IP;電信用戶訪問,就返回一個電信機房的IP。也就是用CDN了! 我們可以部署多臺交換機為不同的用戶服務(wù)。 用戶通過這些交換機訪問后面數(shù)據(jù)中心的Redis Cluster進行秒殺作業(yè)。
我是在springboot + SpringData JPA的環(huán)境下實現(xiàn)的系統(tǒng)。引入了spring-data-redis的依賴
? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-data-redis</artifactId>
? </dependency>
config包下有兩個類
public interface SecKillConfig {
? ? String productId = "1234568"; //這是我數(shù)據(jù)庫中的要秒殺的商品id
}
?這個類的作用主要是配置RedisTemplate,否則使用默認(rèn)的RedisTemplate會使key和value亂碼。
@Configuration
@EnableCaching
public class RedisConfig {
? ? @Bean
? ? public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
? ? ? ? CacheManager cacheManager = new RedisCacheManager(redisTemplate);
? ? ? ? return cacheManager;
? ? }
? ? // 以下兩種redisTemplate自由根據(jù)場景選擇
? ? @Bean
? ? public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
? ? ? ? RedisTemplate<Object, Object> template = new RedisTemplate<>();
? ? ? ? template.setConnectionFactory(connectionFactory);
? ? ? ? Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
? ? ? ? ObjectMapper mapper = new ObjectMapper();
? ? ? ? mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
? ? ? ? mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
? ? ? ? serializer.setObjectMapper(mapper);
? ? ? ? template.setValueSerializer(serializer);
? ? ? ? //使用StringRedisSerializer來序列化和反序列化redis的key值
? ? ? ? template.setKeySerializer(new StringRedisSerializer());//這兩句是關(guān)鍵
? ? ? ? template.setHashKeySerializer(new StringRedisSerializer());//這兩句是關(guān)鍵
? ? ? ? template.afterPropertiesSet();
? ? ? ? return template;
? ? }
? ? @Bean
? ? public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
? ? ? ? StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
? ? ? ? stringRedisTemplate.setConnectionFactory(factory);
? ? ? ? return stringRedisTemplate;
? ? }
} ?
下面是util包
public class KeyUtil {
?
? ? public static ? synchronized String ? getUniqueKey(){
? ? ? ? Random random = new Random();
? ? ? ? Integer num = random.nextInt(100000);
? ? ? ? return ?num.toString()+System.currentTimeMillis();
? ? }
}
public class SecUtils {
? ??
? ? /*
? ? 創(chuàng)建虛擬訂單
? ? ?*/
? ? public ?static SecOrder createDummyOrder(ProductInfo productInfo){
? ? ? ? String key = KeyUtil.getUniqueKey();
? ? ? ? SecOrder secOrder = new SecOrder();
? ? ? ? secOrder.setId(key);
? ? ? ? secOrder.setUserId("userId="+key);
? ? ? ? secOrder.setProductId(productInfo.getProductId());
? ? ? ? secOrder.setProductPrice(productInfo.getProductPrice());
? ? ? ? secOrder.setAmount(productInfo.getProductPrice());
? ? ? ? return secOrder;
? ? }
? ??
? ?/*
? ?偽支付
? ? */
? ? public static ?boolean dummyPay(){
? ? ? ? Random random = new Random();
? ? ? ? int result = random.nextInt(1000) % 2;
? ? ? ? if (result == 0){
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? return false;
? ? }
}
下面是重點,分布式鎖的解決
/**
?* 分布式樂觀鎖
?*/
@Component
@Slf4j
public class RedisLock {
?
? ? @Autowired
? ? private StringRedisTemplate redisTemplate;
?
? ? @Autowired
? ? private ProductService productService;
?
? ? /*
? ? 加鎖
? ? ?*/
? ? public boolean lock(String key,String value){
?
? ? ? ? //setIfAbsent對應(yīng)redis中的setnx,key存在的話返回false,不存在返回true
? ? ? ? if ( redisTemplate.opsForValue().setIfAbsent(key,value)){
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? //兩個問題,Q1超時時間
? ? ? ? String currentValue = redisTemplate.opsForValue().get(key);
? ? ? ? if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){
? ? ? ? ? ? //Q2 在線程超時的時候,多個線程爭搶鎖的問題
? ? ? ? ? ? String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
? ? ? ? ? ? if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
? ? ? ? ? ? ? ? return true;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return false;
? ? }
?
? ? public void unlock(String key ,String value){
? ? ? ? try{
? ? ? ? ? ? String currentValue = redisTemplate.opsForValue().get(key);
? ? ? ? ? ? if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
? ? ? ? ? ? ? ? redisTemplate.opsForValue().getOperations().delete(key);
? ? ? ? ? ? }
? ? ? ? }catch(Exception e){
? ? ? ? ? ? log.error("redis分布上鎖解鎖異常, {}",e);
? ? ? ? }
?
? ? }
?
? ? public SecProductInfo refreshStock(String productId){
? ? ? ? SecProductInfo secProductInfo = new SecProductInfo();
? ? ? ? ProductInfo productInfo = productService.findOne(productId);
? ? ? ? if (productId == null){
? ? ? ? ? ? throw new SellException(203,"秒殺商品不存在");
? ? ? ? }
? ? ? ? try{
? ? ? ? ? ? redisTemplate.opsForValue().set("stock"+productInfo.getProductId(),String.valueOf(productInfo.getProductStock()));
? ? ? ? ? ? String value = redisTemplate.opsForValue().get("stock"+productInfo.getProductId());
? ? ? ? ? ? secProductInfo.setProductId(productId);
? ? ? ? ? ? secProductInfo.setStock(value);
? ? ? ? }catch(Exception e){
? ? ? ? ? ? log.error(e.getMessage());
? ? ? ? }
? ? ? ? return secProductInfo;
?
? ? }
?
}
分布式鎖的實現(xiàn)思路
線程進來之后先執(zhí)行redis的setnx,若是key存在就返回0,否則返回1.返回1即代表拿到鎖,開始執(zhí)行代碼,執(zhí)行完畢之后將key刪除即為解鎖。
存在兩個問題,有可能存在死鎖,就是一個線程執(zhí)行拿到鎖之后,解鎖之前的代碼時出現(xiàn)bug,導(dǎo)致鎖釋放不出來,下一個線程進來之后一直等待上一個線程釋放鎖。解決方案就是加上超時時間,超時過后自行無論執(zhí)行是否成功都將鎖釋放出來。但是又會出現(xiàn)第二個問題,在超時的情況下,多個線程同時等待鎖釋放出來,然后競爭拿到鎖,此時又會出現(xiàn)線程不安全現(xiàn)象,解決方案是使用redis的getandset方法,其中一個線程拿到鎖之后立即將value值改變,同時將oldvalue與原來的value值比較,這樣就保證了多線程競爭鎖的安全性。
下面是業(yè)務(wù)邏輯部分的代碼。
先是controller
@RestController
@Slf4j
@RequestMapping("/skill")
public class SecKillController {
?
? ? @Autowired
? ? private SecKillService secKillService;
?
? ? @Autowired
? ? private StringRedisTemplate stringRedisTemplate;
?
? ? @Resource
? ? private RedisTemplate<String,SecOrder> redisTemplate;
? ?
? ? /*
? ? 下單,同時將訂單信息保存在redis中,隨后將數(shù)據(jù)持久化
? ? ?*/
? ? @GetMapping("/order/{productId}")
? ? public String skill(@PathVariable String productId) throws Exception{
? ? ? ? //判斷是否搶光
? ? ? ? int amount = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
? ? ? ? if (amount >= 2000){
? ? ? ? ? ? return "不好意思,活動結(jié)束啦";
? ? ? ? }
? ? ? ? //初始化搶購商品信息,創(chuàng)建虛擬訂單。
? ? ? ? ProductInfo productInfo = new ProductInfo(productId);
? ? ? ? SecOrder secOrder = SecUtils.createDummyOrder(productInfo);
? ? ? ? //付款,付款時時校驗庫存,如果成功redis存儲訂單信息,庫存加1
? ? ? ? if (!SecUtils.dummyPay()){
? ? ? ? ? ? log.error("付款慢啦搶購失敗,再接再厲哦");
? ? ? ? ? ? return "搶購失敗,再接再厲哦";
? ? ? ? }
? ? ? ? log.info("搶購成功 商品id=:"+ productId);
? ? ? ? //訂單信息保存在redis中
? ? ? ? secKillService.orderProductMockDiffUser(productId,secOrder);
? ? ? ? return "訂單數(shù)量: "+redisTemplate.opsForSet().size("order"+productId)+
? ? ? ? ? ? ? ? " ?剩余數(shù)量:"+(2000 - Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId)));
? ? }
? ? /*
? ? ?在redis中刷新庫存
? ? ?*/
? ? @GetMapping("/refresh/{productId}")
? ? public String ?refreshStock(@PathVariable String productId) throws Exception{
? ? ? ? SecProductInfo secProductInfo = secKillService.refreshStock(productId);
? ? ? ? return "庫存id為 "+productId +" <br> ?庫存總量為 "+secProductInfo.getStock();
? ? }
?
}
Service
@Service
public interface SecKillService {
?
? ? ?long orderProductMockDiffUser(String productId,SecOrder secOrder);
? ? ?
? ? ?SecProductInfo refreshStock(String productId);
}
Impl
@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {
?
? ? @Autowired
? ? private RedisLock redisLock;
?
? ? @Autowired
? ? private SecOrderService secOrderService;
?
? ? @Autowired
? ? private StringRedisTemplate stringRedisTemplate;
?
? ? @Resource
? ? private RedisTemplate<String,SecOrder> redisTemplate;
?
? ? private static final int TIMEOUT = 10 * 1000;
?
? ? @Override
? ? public ?long orderProductMockDiffUser(String productId,SecOrder secOrder) {
?
? ? ? ? //加鎖 setnx
? ? ? ? long orderSize = 0;
? ? ? ? long time = System.currentTimeMillis()+ TIMEOUT;
? ? ? ? boolean lock = redisLock.lock(productId, String.valueOf(time));
? ? ? ? if (!lock){
? ? ? ? ? ? throw ?new SellException(200,"哎呦喂,人太多了");
? ? ? ? }
? ? ? //獲得庫存數(shù)量
? ? ? ? int stockNum = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
? ? ? ? if (stockNum >= 2000) {
? ? ? ? ? ? throw new SellException(150, "活動結(jié)束");
? ? ? ? } else {
? ? ? ? ? ? //倉庫數(shù)量減一
? ? ? ? ? ? stringRedisTemplate.opsForValue().increment("stock"+productId,1);
? ? ? ? ? ? //redis中加入訂單
? ? ? ? ? ? redisTemplate.opsForSet().add("order"+productId,secOrder);
? ? ? ? ? ? orderSize = redisTemplate.opsForSet().size("order"+productId);
? ? ? ? ? ? if (orderSize >= 1000){
? ? ? ? ? ? ? ? //訂單信息持久化,多線程寫入數(shù)據(jù)庫(效率從單線程的9000s提升到了9ms)
? ? ? ? ? ? ? ? Set<SecOrder> members = redisTemplate.opsForSet().members("order"+productId);
? ? ? ? ? ? ? ? List<SecOrder> memberList = new ArrayList<>(members);
? ? ? ? ? ? ? ? CountDownLatch countDownLatch = new CountDownLatch(4);
? ? ? ? ? ? ? ? new Thread(() -> {
? ? ? ? ? ? ? ? ? ? for (int i = 0; i <memberList.size() /4 ; i++) {
? ? ? ? ? ? ? ? ? ? ? ? secOrderService.save(memberList.get(i));
? ? ? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }, "therad1").start();
? ? ? ? ? ? ? ? new Thread(() -> {
? ? ? ? ? ? ? ? ? ? for (int i = memberList.size() /4; i <memberList.size() /2 ; i++) {
? ? ? ? ? ? ? ? ? ? ? ? secOrderService.save(memberList.get(i));
? ? ? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }, "therad2").start();
? ? ? ? ? ? ? ? new Thread(() -> {
? ? ? ? ? ? ? ? ? ? for (int i = memberList.size() /2; i <memberList.size() * 3 / 4 ; i++) {
? ? ? ? ? ? ? ? ? ? ? ? secOrderService.save(memberList.get(i));
? ? ? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }, "therad3").start();
? ? ? ? ? ? ? ? new Thread(() -> {
? ? ? ? ? ? ? ? ? ? for (int i = memberList.size() * 3 / 4; i <memberList.size(); i++) {
? ? ? ? ? ? ? ? ? ? ? ? secOrderService.save(memberList.get(i));
? ? ? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }, "therad4").start();
?
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? countDownLatch.await();
? ? ? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ?log.info("訂單持久化完成");
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? //解鎖
? ? ? ? redisLock.unlock(productId,String.valueOf(time));
? ? ? ? return orderSize;
? ? }
? ? @Override
? ? public SecProductInfo refreshStock(String productId) {
? ? ? ? return redisLock.refreshStock(productId);
? ? }
?
}
還有一些輔助的service,和實體類,不過多解釋,一起貼出來吧,方便大家測試
public interface SecOrderService {
?
? ? ?List<SecOrder> findByProductId(String productId);
?
? ? ?SecOrder save(SecOrder secOrder);
?
}
@Service
public class SecOrderServiceImpl implements SecOrderService {
?
? ? @Autowired
? ? private SecOrderRepository secOrderRepository;
?
? ? @Override
? ? public List<SecOrder> findByProductId(String productId) {
?
? ? ? ? return secOrderRepository.findByProductId(productId);
? ? }
?
? ? public SecOrder save(SecOrder secOrder){
?
? ? ? ? return secOrderRepository.save(secOrder);
? ? }
?
}
public interface SecOrderRepository extends JpaRepository<SecOrder,String> {
?
? ? List<SecOrder> findByProductId(String productId);
?
?
? ? SecOrder save(SecOrder secOrder);
?
}
@Entity
@Data
public class ProductInfo {
?
? ? @Id
? ? private String productId;
? ? /**
? ? ?* 產(chǎn)品名
? ? ?*/
? ? private String productName;
? ? /**
? ? ?* 單價
? ? ?*/
? ? private BigDecimal productPrice;
? ? /**
? ? ?* 庫存
? ? ?*/
? ? private Integer productStock;
? ? /**
? ? ?* 產(chǎn)品描述
? ? ?*/
? ? private String productDescription;
? ? /**
? ? ?* 小圖
? ? ?*/
? ? private String productIcon;
? ? /**
? ? ?* 商品狀態(tài) 0正常 1下架
? ? ?*/
? ? private Integer productStatus = ProductStatusEnum.Up.getCode();
? ? /**
? ? ?* 類目編號
? ? ?*/
? ? private Integer categoryType;
?
? ? /** 創(chuàng)建日期*/
? ? @JsonSerialize(using = Date2LongSerializer.class)
? ? private Date createTime;
? ? /**更新時間 */
? ? @JsonSerialize(using = Date2LongSerializer.class)
? ? private Date updateTime;
?
? ? @JsonIgnore
? ? public ProductStatusEnum getProductStatusEnum(){
? ? ? ? return EnumUtil.getBycode(productStatus,ProductStatusEnum.class);
? ? }
?
? ? public ProductInfo(String productId) {
? ? ? ? this.productId = productId;
? ? ? ? this.productPrice = new BigDecimal(3.2);
? ? }
?
? ? public ProductInfo() {
? ? }
}
@Data
@Entity
public class SecOrder ?implements Serializable{
?
? ? private static final long serialVersionUID = 1724254862421035876L;
?
? ? ? ? @Id
? ? private String id;
? ? private String userId;
? ? private String productId;
? ? private BigDecimal productPrice;
? ? private BigDecimal amount;
?
? ? public SecOrder(String productId) {
? ? ? ? String utilId = KeyUtil.getUniqueKey();
? ? ? ? this.id = utilId;
? ? ? ? this.userId = "userId"+utilId;
? ? ? ? this.productId = productId;
? ? }
?
? ? public SecOrder() {
? ? }
?
? ? @Override
? ? public String toString() {
? ? ? ? return "SecOrder{" +
? ? ? ? ? ? ? ? "id='" + id + '\'' +
? ? ? ? ? ? ? ? ", userId='" + userId + '\'' +
? ? ? ? ? ? ? ? ", productId='" + productId + '\'' +
? ? ? ? ? ? ? ? ", productPrice=" + productPrice +
? ? ? ? ? ? ? ? ", amount=" + amount +
? ? ? ? ? ? ? ? '}';
? ? }
}
@Data
public class SecProductInfo {
?
? ? private String productId;
? ? private String stock;
?
}
---------------------?
作者:javaEE小菜鳥?
來源:CSDN?
原文:https://blog.csdn.net/qq_27631217/article/details/80657271?
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的秒杀系统设计架构与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎样才能做出一份好吃的糕点?
- 下一篇: crontab没有正确重定向导致磁盘in