day05-優惠券秒殺01

来源:https://www.cnblogs.com/liyuelian/archive/2023/04/24/17351032.html
-Advertisement-
Play Games

功能03-優惠券秒殺01 4.功能03-優惠券秒殺 4.1全局唯一ID 4.1.1全局ID生成器 每個店鋪都可以發佈優惠券: 當用戶搶購時,就會生成訂單,並保存到tb_voucher_order這張表中。訂單表如果使用資料庫的自增id就存在一些問題: id的規律性太明顯:用戶可以根據id猜測一些信息 ...


功能03-優惠券秒殺01

4.功能03-優惠券秒殺

4.1全局唯一ID

4.1.1全局ID生成器

每個店鋪都可以發佈優惠券:

image-20230423154152138

當用戶搶購時,就會生成訂單,並保存到tb_voucher_order這張表中。訂單表如果使用資料庫的自增id就存在一些問題:

  1. id的規律性太明顯:用戶可以根據id猜測一些信息,從而非法得到數據
  2. 受單表數據量的限制:由於單張表的數據限制,需要進行分表,而如果每張表都採取自增長,容易出現id重覆,會影響訂單之後的業務,比如說售後服務(因為售後服務一般是根據訂單id來進行的)

解決方案:使用全局ID生成器。

(1)全局ID生成器是一種在分散式系統下用來生成全局唯一ID的工具(也稱為分散式唯一ID),一般要滿足下列特性:

  • 唯一性

  • 高可用

  • 高性能

  • 遞增性

  • 安全性

(2)全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake演算法
  • 資料庫自增

(3)我們這裡使用redis作為全局唯一生成器的實現方案,原因如下:

  1. redis是獨立於資料庫之外的,它只有一個,當所有人都來訪問redis時,它的自增一定是唯一的(唯一性)

  2. 使用redis的集群、主從方案、哨兵功能,可以維持它的高可用性(高可用)

  3. redis具有高性能(高性能)

  4. 可以使用redis的String類型,具有自增性(如:incr命令)(自增性)

    Redis Incr 命令將 key 中儲存的數字值增一

    如果 key 不存在,那麼 key 的值會先被初始化為 0 ,然後再執行 INCR 操作

  5. 為了增加id的安全性,我們不會直接使用自增redis自增的id,而是拼接一些其他信息:(安全性)

    ID構造:時間戳+計數器(使用long類型,共八位元組,64bit)

    • 符號位:1bit,永遠為0

    • 時間戳:31bit,以秒為單位,可以使用約69年

    • 序列號:32bit,秒內的計數器,這樣可以支持每秒產生2^32個不同的ID

      image-20230423175259846

4.2Redis實現全局唯一ID

(1)創建全局ID生成器RedisIdWorker

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**
 * @author 李
 * @version 1.0
 */
@Component
public class RedisIdWorker {
    //開始時間戳(1970-01-01T00:00:00到2022-01-01T00:00:00的秒數)
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    //序列號的位數
    private static final int COUNT_BITS = 32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //public static void main(String[] args) {
    //    //開始時間
    //    LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
    //    //得到1970-01-01T00:00:00Z.到指定時間為止的具體秒數
    //    long second = time.toEpochSecond(ZoneOffset.UTC);
    //    System.out.println(second);//1640995200L
    //}

    public long nextId(String keyPrefix) {
        //1.生成時間戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        //開始時間到當前時間的 時間戳
        long timeStamp = nowSecond - BEGIN_TIMESTAMP;

        //2.生成序列號(keyPrefix代表業務首碼)
        /*
         * Redis的 Incr命令將 key 中儲存的數字值增1,如果key不存在,那麼key的值會先被初始化為0,然後再執行INCR操作。
         * 根據這個特性,我們每一天拼接不同的日期,當做key。也就是說同一天下單採用相同的key,不同天下單採用不同的key
         * 這種方法不僅可以防止訂單號使用完(redis的的自增最多可以有2^64位,我們採取其中32位作計數器),
         * 還可以根據不同的日期,統計該天的訂單數量
         */
        //2.1獲取當前的日期(精確到天)
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2做自增長
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //3.拼接並返回
        //將時間戳左移32位,空出來的右邊32位使用count填充,共64位
        return timeStamp << COUNT_BITS | count;
    }
}

(2)測試類(部分代碼)

@Resource
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);

@Test
public void testIdWorker() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(300);
    //線程,生成100個id
    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println("id" + id);
        }
        latch.countDown();
    };
    long start = System.currentTimeMillis();
    //共執行300次任務
    for (int i = 0; i < 300; i++) {
        es.submit(task);
    }
    //讓所有線程執行完才計時
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("共用時=" + (end - start));
}

關於countdownlatch

countdownlatch名為信號槍:主要的作用是同步協調在多線程的等待於喚醒問題。如果沒有CountDownLatch ,由於程式是非同步的,當非同步程式沒有執行完時,主線程可能就已經執行完了。如果期望的是分線程全部走完之後,主線程再走,此時就需要使用到CountDownLatch。CountDownLatch 中有兩個最重要的方法:1.countDown 2.await

await 方法是阻塞方法,使用await可以讓main線程阻塞,當CountDownLatch 內部維護的變數變為0時,就不再阻塞,直接放行。那麼什麼時候CountDownLatch 維護的變數變為0 呢?我們只需要調用一次countDown ,內部變數就減少1。

根據這個性質,讓分線程和變數綁定, 執行完一個分線程就減少一個變數,當分線程全部走完,CountDownLatch 維護的變數就是0,此時await就不再阻塞,統計出來的時間也就是所有分線程執行完後的時間。

測試結果:

image-20230423191756438

查看redis中的數據:對應的key的自增值已經變為30000,說明生成了3w個id

image-20230423195906103

4.2.1總結

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake演算法
  • 資料庫自增(使用一張表來單獨記錄id)

Redis自增ID策略:

  • 每天一個key,方便統計訂單量
  • ID結構:時間戳+計數器

4.2實現優惠券秒殺下單

4.2.1需求分析&業務流程

每個店鋪都可以發佈優惠券,分為平價券和特價券。平價券可以任意購買,而特價券需要秒殺搶購:

image-20230423201358147

這兩張券對應的資料庫表結構如下:

  • tb_voucher:(優惠券表)優惠券的基本信息、優惠金額、使用規則等(包括平價券和秒殺券)

    image-20230424161312210
  • tb_seckill_voucher:(秒殺優惠券表)優惠券的庫存、開始搶購時間、結束搶購時間。秒殺優惠券才需要填寫這些信息。

    image-20230424161158004

要求在店鋪詳情中實現下單購買秒殺券:

下單時需要判斷兩點:

  1. 秒殺是否開始或者結束,如果尚未開始或者已經結束則無法下單
  2. 秒殺券的庫存是否充足,不足則無法下單
image-20230424152833646

優惠券訂單表結構:

image-20230424161458352

業務流程分析:

image-20230424153759552

4.2.2代碼實現

(1)優惠券訂單實體:VoucherOrder.java

package com.hmdp.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 優惠券訂單實體
 * 
 * @author 李
 * @version 1.0
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher_order")
public class VoucherOrder implements Serializable {
    private static final long serialVersionUID = 1L;
    //主鍵
    @TableId(value = "id", type = IdType.INPUT)
    private Long id;
    //下單的用戶id
    private Long userId;
    //購買的代金券id
    private Long voucherId;
    //支付方式 1:餘額支付;2:支付寶;3:微信
    private Integer payType;
    //訂單狀態,1:未支付;2:已支付;3:已核銷;4:已取消;5:退款中;6:已退款
    private Integer status;
    //下單時間
    private LocalDateTime createTime;
    //支付時間
    private LocalDateTime payTime;
    //核銷時間
    private LocalDateTime useTime;
    //退款時間
    private LocalDateTime refundTime;
    //更新時間
    private LocalDateTime updateTime;
}

(2)mapper介面

package com.hmdp.mapper;

import com.hmdp.entity.VoucherOrder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 *  Mapper 介面
 *
 * @author 李
 * @version 1.0
 */
public interface VoucherOrderMapper extends BaseMapper<VoucherOrder> {

}

(3)IVoucherOrderService 服務類

package com.hmdp.service;

import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 *  服務類
 *
 * @author 李
 * @version 1.0
 */
public interface IVoucherOrderService extends IService<VoucherOrder> {

    Result seckillVoucher(Long voucherId);
}

(4)VoucherOrderServiceImpl 服務實現類

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * 服務實現類
 *
 * @author 李
 * @version 1.0
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //根據id查詢優惠券信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher == null) {
            return Result.fail("該優惠券不存在,請刷新!");
        }
        //判斷秒殺券是否在有效時間內
        //若不在有效期,則返回異常結果
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒殺尚未開始!");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒殺已經結束!");
        }
        //若在有效期,判斷庫存是否充足
        if (voucher.getStock() < 1) {//庫存不足
            return Result.fail("秒殺券庫存不足!");
        }
        //庫存充足,則扣減庫存(操作秒殺券表)
        boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();
        if (!success) {//操作失敗
            return Result.fail("秒殺券庫存不足!");
        }
        //扣減庫存成功,則創建訂單,返回訂單id
        VoucherOrder voucherOrder = new VoucherOrder();
        //設置訂單id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //設置用戶id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //設置代金券id
        voucherOrder.setVoucherId(voucherId);

        //將訂單寫入資料庫(操作優惠券訂單表)
        this.save(voucherOrder);

        //返回訂單id
        return Result.ok(orderId);
    }
}

(5)控制器 VoucherOrderController

package com.hmdp.controller;


import com.hmdp.dto.Result;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.service.IVoucherService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 秒殺券前端控制器
 *
 * @author 李
 * @version 1.0
 */
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

(6)測試,在前端頁面點擊購買,顯示搶購成功,訂單號如下:

image-20230424163828781

優惠券訂單表tb_voucher_order成功插入一條數據:

image-20230424164101927

對應的秒殺券的庫存減一:

image-20230424164300622

4.3超賣問題

4.3.1問題分析

4.2的代碼並沒有考慮到併發的問題:當有多個用戶同時對一個秒殺券進行搶購,併發會讓系統出現超賣問題:即賣出的秒殺券數量>實際的秒殺券庫存

我們使用jemeter測試:

image-20230424165758548 image-20230424165828025 image-20230424170309980

運行上述設置,測試結果如下:

  1. 秒殺券表中,id=2的秒殺券庫存出現了負數:

    image-20230424170139020
  2. 訂單表中,對應的數量為104單,但是對應的秒殺券的庫存最多只有100張。也就是說:出現了超賣問題

    image-20230424170544754

出現超賣問題的原因:

4.2的代碼只是簡單地進行庫存判斷,並沒有考慮到線程併發。當有多個線程同時去判斷庫存時,如果當前庫存大於0,則這些線程都會去進行庫存扣減,從而發生併發安全問題:

image-20230424171155754 image-20230424171918982

4.3.2解決方案

超賣問題是典型的多線程安全問題,針對這一問題的常見解決方案就是加鎖:

image-20230424172233037

這裡使用樂觀鎖方案。樂觀鎖的關鍵是判斷之前查詢到的數據是否有被修改過:

常見的方式有兩種:

(1)版本號法:

表中設置一個版本號欄位,線程在修改表之前,先查詢一次版本號。對資料庫表操作時,再查詢一次版本號,如果值和之前的一致,說明此時表的數據在兩次查詢之間沒有被修改過,我們就可以進行業務操作,並設置新的版本號。

update語句會對當前修改的行進行鎖定操作(資料庫有行級鎖,不用擔心一行記錄被同時修改)。

因此,進行表修改時,由於資料庫行鎖,其他線程會等待數據修改後再更新庫存

sql執行是交給資料庫的,如果開啟了事務的話,就是兩個事務的併發問題,此時將會啟動兩階段封鎖協議,保證事務併發安全

image-20230424180914306

image-20230424181018721 image-20230424181058145

(2)CAS法:

這裡為了簡化,使用庫存代替版本號,原理和方案1是一致的:線程在修改表之前,先查詢一次庫存的值。對資料庫表操作時,再查詢一次庫存值,如果值和之前的一致,說明此時表的數據在兩次查詢之間沒有被修改過,我們就可以進行業務操作。

image-20230424183341487

image-20230424183557589 image-20230424183641411

CAS思想:Compare-And-Swap

CAS 有三個操作數:記憶體值 V、預期值 A、要修改的值 B。CAS 最核心的思路就是,僅當預期值 A 和當前的記憶體值 V 相同時,才將記憶體值修改為 B。

ABA問題

為了簡便,這裡使用方案2,但實際的業務還是建議使用版本法來避免其他問題。

4.3.3代碼實現

(1)修改VoucherOrderServiceImpl,添加如下代碼:

image-20230424191811284

(2)測試:

清除之前的訂單信息(tb_voucher_order):

image-20230424192136997

還原tb_seckill_voucher表的測試數據:

image-20230424192255563

然後使用jemeter進行測試:

image-20230424204420148 image-20230424204442421

測試結果:

券沒有超賣,但是出現了新的問題:前幾個請求中就出現了下單失敗的情況,200個線程只有100-63=37個線程下單成功(理想情況下是100,即秒殺券全部賣出)

image-20230424204357434

原因分析:這是因為,當有一個線程去修改數據時,其他很多的線程也來同時請求,它們都根據第一次查詢的stock值去判斷,發現stock值變化了,因此當第一個線程修改數據後,都沒有去對數據進行操作),導致發生了庫存充足,仍然搶不到券的情況(搶券失敗率偏高)。

(3)改進:修改VoucherOrderServiceImpl,修改如下劃線處:

分析:線程A獲取stock值,通過業務判斷,然後去對庫存值進行update操作;因為update語句會對當前修改的行進行鎖定操作,因此,進行表修改時,由於資料庫行鎖,其他線程會等待數據修改後再更新庫存。當等待後獲取鎖,將where stock > 0作為update條件,這時,只要stock不小於0就仍可以售券。

update where 是先走where去拿鎖,拿不到就阻塞,等拿到鎖了再去執行update

image-20230424205453393

再次對其測試:可以看到200個線程併發,100張秒殺券全部售完。並且沒有出現超賣現象,同時解決了庫存充足卻搶不到券的問題。

image-20230424212235502

4.3.4總結

超賣這樣的線程安全問題,解決方案有哪些?

  1. 悲觀鎖:添加同步鎖,讓線程串列執行
    • 優點:簡答粗暴
    • 缺點:性能一般
  2. 樂觀鎖:不加鎖,在更新時判斷是否有其他線程在修改
    • 優點:性能好
    • 缺點:成功率低

4.4一人一單

4.5分散式鎖

4.6Redis優化秒殺

4.7Redis消息隊列實現非同步秒殺


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一:背景 1. 講故事 上周有位朋友找到我,說他的 API 被多次調用後出現了記憶體暴漲,讓我幫忙看下是怎麼回事?看樣子是有些擔心,但也不是特別擔心,那既然找到我,就給他分析一下吧。 二:WinDbg 分析 1. 到底是哪裡的泄露 這也是我一直在訓練營灌輸的理念,一定要知道是哪一邊的暴漲,否則很可能就 ...
  • 飛騰愛好者技術交流群碼公眾號“烏拉大喵喵” 顆粒線序配置輔助工具 B站講解視頻: 正文內容: 一、 飛騰X100顯存使用LPDDR4時,需要工程師在X100的固件中去配置線序交換說明,就類似下麵這個: 圖1 我們需要輸入每個slice中DQ的線序,也需要輸入slice之間的交換關係,這個工作量也不小 ...
  • 一、 前言 搭建和維護集群環境中時鐘同步是非常重要一環。如果集群的時間不統一,例如ceph集群就會報錯無法更新數據、CDH集群無法添加客戶端等等。目前主流在Linux系統搭建集群用到NTP和chrony軟體,本文簡單介紹兩者的集群搭建。 二、 NTP和chrony區別 根據chrony官網描述,主要 ...
  • 使用華為雲鯤鵬彈性雲伺服器部署Discuz 華為雲配置地址: 實驗目的與基本要求 實驗目的: Discuz!是一套通用社區論壇軟體系統,用戶在不需要任何編程的基礎上,通過簡單的設置和安裝,在互聯網上搭建起具備完善功能、很強負載能力和可高度定製的論壇服務。實驗將在華為雲鯤鵬彈性雲伺服器CentOS系統 ...
  • 伺服器環境:windows server,phpstudy,Apache 一、阿裡雲下載SSL證書 1、前往阿裡雲官網 登錄進入控制台,搜索“SSL證書(應用安全)”,點擊左側“SSL證書”菜單,點擊“免費證書” 2、購買證書 點擊“立即購買”,選擇配置,支付金額為0元,即為免費。 3、創建證書 購 ...
  • 前言 錄這套教程主要幫助那些對Linux瞭解很少但又想做一個初步學習的小伙伴,因為我們一般在找開發相關的工作面試時偶爾也會被問到,而且做為一名開發人員如果不會操作Linux操作系統確實也有些說不過去,因為一般我們在企業中用到的中間件、服務的部署等都是在Linux上,本期教程就帶著大家來入門下Linu ...
  • 一款輕量級、高性能、功能強大的內網穿透代理伺服器。支持tcp、udp、socks5、http等幾乎所有流量轉發,並帶有功能強大的web管理端。 ...
  • 記錄 mongo 資料庫用原生自帶的命令工具使用 json 文件方式進行導入、導出的操作! 在一次數據更新中,同事把老數據進行了清空操作,但是新的邏輯數據由於某種原因(好像是她的電腦中病毒了),一直無法正常連接資料庫進行數據插入,然後下午2點左右要給甲方演示,所以要緊急恢複本地的部分數據到生產庫。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...