MapReduce多種join實現實例分析(二)

来源:https://www.cnblogs.com/shsxt/archive/2017/12/27/8127760.html
-Advertisement-
Play Games

上一篇《MapReduce多種join實現實例分析(一)》,大家可以點擊回顧該篇文章。本文是MapReduce系列第二篇。 一、在Map端進行連接使用場景:一張表十分小、一張表很大。用法:在提交作業的時候先將小表文件放到該作業的DistributedCache中,然後從DistributeCache ...


上一篇《MapReduce多種join實現實例分析(一)》,大家可以點擊回顧該篇文章。本文是MapReduce系列第二篇。

一、在Map端進行連接
使用場景:一張表十分小、一張表很大。
用法:在提交作業的時候先將小表文件放到該作業的DistributedCache中,然後從DistributeCache中取出該小表進行join key / value解釋分割放到記憶體中(可以放大Hash Map等等容器中)。然後掃描大表,看大表中的每條記錄的join key /value值是否能夠在記憶體中找到相同join key的記錄,如果有則直接輸出結果。
直接上代碼,比較簡單:

package com.mr.mapSideJoin;   
import java.io.BufferedReader;   
import java.io.FileReader;   
import java.io.IOException;   
import java.util.HashMap;   
import org.apache.hadoop.conf.Configuration;   
import org.apache.hadoop.conf.Configured;   
import org.apache.hadoop.filecache.DistributedCache;   
import org.apache.hadoop.fs.Path;   
import org.apache.hadoop.io.Text;   
import org.apache.hadoop.mapreduce.Job;   
import org.apache.hadoop.mapreduce.Mapper;   
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;   
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;   
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;   
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;   
import org.apache.hadoop.util.Tool;   
import org.apache.hadoop.util.ToolRunner;   
import org.slf4j.Logger;   
import org.slf4j.LoggerFactory;   
/** 
 * @author zengzhaozheng 
 * 
 * 用途說明: 
 * Map side join中的left outer join 
 * 左連接,兩個文件分別代表2個表,連接欄位table1的id欄位和table2的cityID欄位 
 * table1(左表):tb_dim_city(id int,name string,orderid int,city_code,is_show), 
 * 假設tb_dim_city文件記錄數很少,tb_dim_city.dat文件內容,分隔符為"|": 
 * id     name  orderid  city_code  is_show 
 * 0       其他        9999     9999         0 
 * 1       長春        1        901          1 
 * 2       吉林        2        902          1 
 * 3       四平        3        903          1 
 * 4       松原        4        904          1 
 * 5       通化        5        905          1 
 * 6       遼源        6        906          1 
 * 7       白城        7        907          1 
 * 8       白山        8        908          1 
 * 9       延吉        9        909          1 
 * -------------------------風騷的分割線------------------------------- 
 * table2(右表):tb_user_profiles(userID int,userName string,network string,double flow,cityID int) 
 * tb_user_profiles.dat文件內容,分隔符為"|": 
 * userID   network     flow    cityID 
 * 1           2G       123      1 
 * 2           3G       333      2 
 * 3           3G       555      1 
 * 4           2G       777      3 
 * 5           3G       666      4 
 * -------------------------風騷的分割線------------------------------- 
 *  結果: 
 *  1   長春  1   901 1   1   2G  123 
 *  1   長春  1   901 1   3   3G  555 
 *  2   吉林  2   902 1   2   3G  333 
 *  3   四平  3   903 1   4   2G  777 
 *  4   松原  4   904 1   5   3G  666 
 */
public class MapSideJoinMain extends Configured implements Tool{   
    private static final Logger logger = LoggerFactory.getLogger(MapSideJoinMain.class);   
    public static class LeftOutJoinMapper extends Mapper<Object, Text, Text, Text> {
 
        private HashMap<String,String> city_info = new HashMap<String, String>();   
        private Text outPutKey = new Text();   
        private Text outPutValue = new Text();   
        private String mapInputStr = null;   
        private String mapInputSpit[] = null;   
        private String city_secondPart = null;   
        /** 
         * 此方法在每個task開始之前執行,這裡主要用作從DistributedCache 
         * 中取到tb_dim_city文件,並將裡邊記錄取出放到記憶體中。 
         */
        @Override
        protected void setup(Context context)   
                throws IOException, InterruptedException {   
            BufferedReader br = null;   
            //獲得當前作業的DistributedCache相關文件 
            Path[] distributePaths = DistributedCache.getLocalCacheFiles(context.getConfiguration());   
            String cityInfo = null;   
            for(Path p : distributePaths){   
                if(p.toString().endsWith("tb_dim_city.dat")){   
                    //讀緩存文件,並放到mem中 
                    br = new BufferedReader(new FileReader(p.toString()));   
                    while(null!=(cityInfo=br.readLine())){   
                        String[] cityPart = cityInfo.split("\\|",5);   
                        if(cityPart.length ==5){   
                            city_info.put(cityPart[0], cityPart[1]+"\t"+cityPart[2]+"\t"+cityPart[3]+"\t"+cityPart[4]);   
                        }   
                    }   
                }   
            }   
        }
 
        /** 
         * Map端的實現相當簡單,直接判斷tb_user_profiles.dat中的 
         * cityID是否存在我的map中就ok了,這樣就可以實現Map Join了 
         */
        @Override
        protected void map(Object key, Text value, Context context)   
                throws IOException, InterruptedException {   
            //排掉空行 
            if(value == null || value.toString().equals("")){   
                return;   
            }   
            mapInputStr = value.toString();   
            mapInputSpit = mapInputStr.split("\\|",4);   
            //過濾非法記錄 
            if(mapInputSpit.length != 4){   
                return;   
            }   
            //判斷鏈接欄位是否在map中存在 
            city_secondPart = city_info.get(mapInputSpit[3]);   
            if(city_secondPart != null){   
                this.outPutKey.set(mapInputSpit[3]);   
                this.outPutValue.set(city_secondPart+"\t"+mapInputSpit[0]+"\t"+mapInputSpit[1]+"\t"+mapInputSpit[2]);   
                context.write(outPutKey, outPutValue);   
            }   
        }   
    }   
    @Override
    public int run(String[] args) throws Exception {   
            Configuration conf=getConf(); //獲得配置文件對象 
            DistributedCache.addCacheFile(new Path(args[1]).toUri(), conf);//為該job添加緩存文件 
            Job job=new Job(conf,"MapJoinMR");   
            job.setNumReduceTasks(0);
 
            FileInputFormat.addInputPath(job, new Path(args[0])); //設置map輸入文件路徑 
            FileOutputFormat.setOutputPath(job, new Path(args[2])); //設置reduce輸出文件路徑
 
            job.setJarByClass(MapSideJoinMain.class);   
            job.setMapperClass(LeftOutJoinMapper.class);
 
            job.setInputFormatClass(TextInputFormat.class); //設置文件輸入格式 
            job.setOutputFormatClass(TextOutputFormat.class);//使用預設的output格式
 
            //設置map的輸出key和value類型 
            job.setMapOutputKeyClass(Text.class);
 
            //設置reduce的輸出key和value類型 
            job.setOutputKeyClass(Text.class);   
            job.setOutputValueClass(Text.class);   
            job.waitForCompletion(true);   
            return job.isSuccessful()?0:1;   
    }   
    public static void main(String[] args) throws IOException,   
            ClassNotFoundException, InterruptedException {   
        try {   
            int returnCode =  ToolRunner.run(new MapSideJoinMain(),args);   
            System.exit(returnCode);   
        } catch (Exception e) {   
            // TODO Auto-generated catch block 
            logger.error(e.getMessage());   
        }   
    }   
} 

  

這裡說說DistributedCache。DistributedCache是分散式緩存的一種實現,它在整個MapReduce框架中起著相當重要的作用,他可以支撐我們寫一些相當複雜高效的分散式程式。說回到這裡,JobTracker在作業啟動之前會獲取到DistributedCache的資源uri列表,並將對應的文件分發到各個涉及到該作業的任務的TaskTracker上。另外,關於DistributedCache和作業的關係,比如許可權、存儲路徑區分、public和private等屬性,接下來有用再整理研究一下寫一篇blog,這裡就不詳細說了。

另外還有一種比較變態的Map Join方式,就是結合HBase來做Map Join操作。這種方式完全可以突破記憶體的控制,使你毫無忌憚的使用Map Join,而且效率也非常不錯。

二、SemiJoin
SemiJoin就是所謂的半連接,其實仔細一看就是reduce join的一個變種,就是在map端過濾掉一些數據,在網路中只傳輸參與連接的數據不參與連接的數據不必在網路中進行傳輸,從而減少了shuffle的網路傳輸量,使整體效率得到提高,其他思想和reduce join是一模一樣的。說得更加接地氣一點就是將小表中參與join的key單獨抽出來通過DistributedCach分發到相關節點,然後將其取出放到記憶體中(可以放到HashSet中),在map階段掃描連接表,將join key不在記憶體HashSet中的記錄過濾掉,讓那些參與join的記錄通過shuffle傳輸到reduce端進行join操作,其他的和reduce join都是一樣的。

看代碼:

package com.mr.SemiJoin;   
import java.io.BufferedReader;   
import java.io.FileReader;   
import java.io.IOException;   
import java.util.ArrayList;   
import java.util.HashSet;   
import org.apache.hadoop.conf.Configuration;   
import org.apache.hadoop.conf.Configured;   
import org.apache.hadoop.filecache.DistributedCache;   
import org.apache.hadoop.fs.Path;   
import org.apache.hadoop.io.Text;   
import org.apache.hadoop.mapreduce.Job;   
import org.apache.hadoop.mapreduce.Mapper;   
import org.apache.hadoop.mapreduce.Reducer;   
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;   
import org.apache.hadoop.mapreduce.lib.input.FileSplit;   
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;   
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;   
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;   
import org.apache.hadoop.util.Tool;   
import org.apache.hadoop.util.ToolRunner;   
import org.slf4j.Logger;   
import org.slf4j.LoggerFactory;   
/** 
 * @author zengzhaozheng 
 * 
 * 用途說明: 
 * reudce side join中的left outer join 
 * 左連接,兩個文件分別代表2個表,連接欄位table1的id欄位和table2的cityID欄位 
 * table1(左表):tb_dim_city(id int,name string,orderid int,city_code,is_show) 
 * tb_dim_city.dat文件內容,分隔符為"|": 
 * id     name  orderid  city_code  is_show 
 * 0       其他        9999     9999         0 
 * 1       長春        1        901          1 
 * 2       吉林        2        902          1 
 * 3       四平        3        903          1 
 * 4       松原        4        904          1 
 * 5       通化        5        905          1 
 * 6       遼源        6        906          1 
 * 7       白城        7        907          1 
 * 8       白山        8        908          1 
 * 9       延吉        9        909          1 
 * -------------------------風騷的分割線------------------------------- 
 * table2(右表):tb_user_profiles(userID int,userName string,network string,double flow,cityID int) 
 * tb_user_profiles.dat文件內容,分隔符為"|": 
 * userID   network     flow    cityID 
 * 1           2G       123      1 
 * 2           3G       333      2 
 * 3           3G       555      1 
 * 4           2G       777      3 
 * 5           3G       666      4 
 * -------------------------風騷的分割線------------------------------- 
 * joinKey.dat內容: 
 * city_code 
 * 1 
 * 2 
 * 3 
 * 4 
 * -------------------------風騷的分割線------------------------------- 
 *  結果: 
 *  1   長春  1   901 1   1   2G  123 
 *  1   長春  1   901 1   3   3G  555 
 *  2   吉林  2   902 1   2   3G  333 
 *  3   四平  3   903 1   4   2G  777 
 *  4   松原  4   904 1   5   3G  666 
 */
public class SemiJoin extends Configured implements Tool{   
    private static final Logger logger = LoggerFactory.getLogger(SemiJoin.class);   
    public static class SemiJoinMapper extends Mapper<Object, Text, Text, CombineValues> {   
        private CombineValues combineValues = new CombineValues();   
        private HashSet<String> joinKeySet = new HashSet<String>();   
        private Text flag = new Text();   
        private Text joinKey = new Text();   
        private Text secondPart = new Text();   
        /** 
         * 將參加join的key從DistributedCache取出放到記憶體中,以便在map端將要參加join的key過濾出來。b 
         */
        @Override
        protected void setup(Context context)   
                throws IOException, InterruptedException {   
            BufferedReader br = null;   
            //獲得當前作業的DistributedCache相關文件 
            Path[] distributePaths = DistributedCache.getLocalCacheFiles(context.getConfiguration());   
            String joinKeyStr = null;   
            for(Path p : distributePaths){   
                if(p.toString().endsWith("joinKey.dat")){   
                    //讀緩存文件,並放到mem中 
                    br = new BufferedReader(new FileReader(p.toString()));   
                    while(null!=(joinKeyStr=br.readLine())){   
                        joinKeySet.add(joinKeyStr);   
                    }   
                }   
            }   
        }   
        @Override
        protected void map(Object key, Text value, Context context)   
                throws IOException, InterruptedException {   
            //獲得文件輸入路徑 
            String pathName = ((FileSplit) context.getInputSplit()).getPath().toString();   
            //數據來自tb_dim_city.dat文件,標誌即為"0" 
            if(pathName.endsWith("tb_dim_city.dat")){   
                String[] valueItems = value.toString().split("\\|");   
                //過濾格式錯誤的記錄 
                if(valueItems.length != 5){   
                    return;   
                }   
                //過濾掉不需要參加join的記錄 
                if(joinKeySet.contains(valueItems[0])){   
                    flag.set("0");   
                    joinKey.set(valueItems[0]);   
                    secondPart.set(valueItems[1]+"\t"+valueItems[2]+"\t"+valueItems[3]+"\t"+valueItems[4]);   
                    combineValues.setFlag(flag);   
                    combineValues.setJoinKey(joinKey);   
                    combineValues.setSecondPart(secondPart);   
                    context.write(combineValues.getJoinKey(), combineValues);   
                }else{   
                    return ;   
                }   
            }//數據來自於tb_user_profiles.dat,標誌即為"1" 
            else if(pathName.endsWith("tb_user_profiles.dat")){   
                String[] valueItems = value.toString().split("\\|");   
                //過濾格式錯誤的記錄 
                if(valueItems.length != 4){   
                    return;   
                }   
                //過濾掉不需要參加join的記錄 
                if(joinKeySet.contains(valueItems[3])){   
                    flag.set("1");   
                    joinKey.set(valueItems[3]);   
                    secondPart.set(valueItems[0]+"\t"+valueItems[1]+"\t"+valueItems[2]);   
                    combineValues.setFlag(flag);   
                    combineValues.setJoinKey(joinKey);   
                    combineValues.setSecondPart(secondPart);   
                    context.write(combineValues.getJoinKey(), combineValues);   
                }else{   
                    return ;   
                }   
            }   
        }   
    }   
    public static class SemiJoinReducer extends Reducer<Text, CombineValues, Text, Text> {   
        //存儲一個分組中的左表信息 
        private ArrayList<Text> leftTable = new ArrayList<Text>();   
        //存儲一個分組中的右表信息 
        private ArrayList<Text> rightTable = new ArrayList<Text>();   
        private Text secondPar = null;   
        private Text output = new Text();   
        /** 
         * 一個分組調用一次reduce函數 
         */
        @Override
        protected void reduce(Text key, Iterable<CombineValues> value, Context context)   
                throws IOException, InterruptedException {   
            leftTable.clear();   
            rightTable.clear();   
            /** 
             * 將分組中的元素按照文件分別進行存放 
             * 這種方法要註意的問題: 
             * 如果一個分組內的元素太多的話,可能會導致在reduce階段出現OOM, 
             * 在處理分散式問題之前最好先瞭解數據的分佈情況,根據不同的分佈採取最 
             * 適當的處理方法,這樣可以有效的防止導致OOM和數據過度傾斜問題。 
             */
            for(CombineValues cv : value){   
                secondPar = new Text(cv.getSecondPart().toString());   
                //左表tb_dim_city 
                if("0".equals(cv.getFlag().toString().trim())){   
                    leftTable.add(secondPar);   
                }   
                //右表tb_user_profiles 
                else if("1".equals(cv.getFlag().toString().trim())){   
                    rightTable.add(secondPar);   
                }   
            }   
            logger.info("tb_dim_city:"+leftTable.toString());   
            logger.info("tb_user_profiles:"+rightTable.toString());   
            for(Text leftPart : leftTable){   
                for(Text rightPart : rightTable){   
                    output.set(leftPart+ "\t" + rightPart);   
                    context.write(key, output);   
                }   
            }   
        }   
    }   
    @Override
    public int run(String[] args) throws Exception {   
            Configuration conf=getConf(); //獲得配置文件對象 
            DistributedCache.addCacheFile(new Path(args[2]).toUri(), conf);
            Job job=new Job(conf,"LeftOutJoinMR");   
            job.setJarByClass(SemiJoin.class);
 
            FileInputFormat.addInputPath(job, new Path(args[0])); //設置map輸入文件路徑 
            FileOutputFormat.setOutputPath(job, new Path(args[1])); //設置reduce輸出文件路徑
 
            job.setMapperClass(SemiJoinMapper.class);   
            job.setReducerClass(SemiJoinReducer.class);
 
            job.setInputFormatClass(TextInputFormat.class); //設置文件輸入格式 
            job.setOutputFormatClass(TextOutputFormat.class);//使用預設的output格式
 
            //設置map的輸出key和value類型 
            job.setMapOutputKeyClass(Text.class);   
            job.setMapOutputValueClass(CombineValues.class);
 
            //設置reduce的輸出key和value類型 
            job.setOutputKeyClass(Text.class);   
            job.setOutputValueClass(Text.class);   
            job.waitForCompletion(true);   
            return job.isSuccessful()?0:1;   
    }   
    public static void main(String[] args) throws IOException,   
            ClassNotFoundException, InterruptedException {   
        try {   
            int returnCode =  ToolRunner.run(new SemiJoin(),args);   
            System.exit(returnCode);   
        } catch (Exception e) {   
            logger.error(e.getMessage());   
        }   
    }   
} 

  

這裡還說說SemiJoin也是有一定的適用範圍的,其抽取出來進行join的key是要放到記憶體中的,所以不能夠太大,容易在Map端造成OOM。

三、總結
blog介紹了三種join方式。這三種join方式適用於不同的場景,其處理效率上的相差還是蠻大的,其中主要導致因素是網路傳輸。Map join效率最高,其次是SemiJoin,最低的是reduce join。另外,寫分散式大數據處理程式的時最好要對整體要處理的數據分佈情況作一個瞭解,這可以提高我們代碼的效率,使數據的傾斜度降到最低,使我們的代碼傾向性更好。

本文寫作過程中參考了上海尚學堂相關技術文章,在此感謝上海尚學堂老師的幫助。

 


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

-Advertisement-
Play Games
更多相關文章
  • 作為Windows as a Service系列的收官文,我將為大家分享使用Intune來管理Windows 10更新的方法。由於Microsoft Intune服務目前尚未在中國落地,感興趣的小伙伴可以申請全球版的Microsoft Intune的測試帳號來試一下...Magic~ ...
  • System Center Configuration Manager (SCCM)為我們環境中的Windows 10客戶端管理和更新提供了一種簡單的機制,給予了我們管理Windows 10更新的最大控制權。要管理Windows 10功能更新,System Center Configuration ... ...
  • 這篇文檔將為大家分享使用WSUS管理Windows 10更新的方法。Windows Server Update Services (WSUS)是微軟提供給客戶免費將Windows10更新部署至運行了Windows 10操作系統的電腦上的方案。我將從如下幾個步驟展開介紹: 1.創建電腦組 2.... ...
  • 簡述 這是我第一次完整的自己黑蘋果,目標是搭建一臺高性能的蘋果主機,用於開發和設計。聽說10.13系統還不是特別的穩定,這次就決定安裝10.12版本的 藉助的tonymacx86上的一篇 "文章" ,鑒於爬坑成功,就將我自己的經驗結合這篇文章做一個完整的中文版本 本文總共分為四個板塊:簡述、軟硬體準 ...
  • 目前打造完成的IDE主要有: terminator+Bundle+NERDtree+YCF(youcompleteme)+UltiSnips+新創建文件自動補充註釋和作者,版權信息等 1,當任務比較多的時候,如果在Ubuntu下切換多個終端,會比較麻煩,這裡我找到一個比較好的終端(terminato ...
  • 第一個裡程碑:檢查系統是否支持ppp [root@m01 ~]# cat /dev/ppp cat: /dev/ppp: No such device or address 如果出現以上提示則說明ppp是開啟的,可以正常架設pptp服務,若出現Permission denied等其他提示,你需要先去 ...
  • 1. 前言 前一篇配置SQL Server on Linux(1),地址:http://www.cnblogs.com/fishparadise/p/8125203.html ,是關於更改資料庫排序規則的。實現的原理跟在Windows平臺差不多,都是需要備份用戶資料庫,重建系統資料庫來實現的,不過操 ...
  • PL/SQL Developer使用技巧(部分) 關鍵字自動大寫 在sql命令視窗中輸入SQL語句時,想要關鍵字自動大寫,引人註目該怎麼辦呢? 一步設置就可以達成了。點擊Tools->Preference->Editor,看到截圖中這個Keyword case,一般預設是Unchanged,在下拉框 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...