国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁 > news >正文

武漢自適應(yīng)網(wǎng)站沈陽線上教學(xué)

武漢自適應(yīng)網(wǎng)站,沈陽線上教學(xué),網(wǎng)站備案中頁面,劉濤做的兒童購物網(wǎng)站在互聯(lián)網(wǎng)的業(yè)務(wù)系統(tǒng)中,涉及到各種各樣的ID,如在支付系統(tǒng)中就會(huì)有支付ID、退款I(lǐng)D等。那一般生成ID都有哪些解決方案呢?特別是在復(fù)雜的分布式系統(tǒng)業(yè)務(wù)場(chǎng)景中,我們應(yīng)該采用哪種適合自己的解決方案是十分重要的。下面我們一一來列舉一…

在互聯(lián)網(wǎng)的業(yè)務(wù)系統(tǒng)中,涉及到各種各樣的ID,如在支付系統(tǒng)中就會(huì)有支付ID、退款I(lǐng)D等。那一般生成ID都有哪些解決方案呢?特別是在復(fù)雜的分布式系統(tǒng)業(yè)務(wù)場(chǎng)景中,我們應(yīng)該采用哪種適合自己的解決方案是十分重要的。下面我們一一來列舉一下,不一定全部適合,這些解決方案僅供參考,或許對(duì)你有用。

一、分布式ID

1.什么是分布式ID

日常開發(fā)中,我們需要對(duì)系統(tǒng)中的各種數(shù)據(jù)使用 ID 唯一表示,比如 用戶 ID 對(duì)應(yīng)且僅對(duì)應(yīng)一個(gè)人,商品 ID 對(duì)應(yīng)且僅對(duì)應(yīng)一件商品,訂 單 ID 對(duì)應(yīng)且僅對(duì)應(yīng)一個(gè)訂單。

拿MySQL數(shù)據(jù)庫舉個(gè)例子:
在我們業(yè)務(wù)數(shù)據(jù)量不大的時(shí)候,單庫單表完全可以支撐現(xiàn)有業(yè)務(wù),數(shù)據(jù)再大一點(diǎn)搞個(gè)MySQL主從同步讀寫分離也能對(duì)付。
但隨著數(shù)據(jù)日漸增長,主從同步也扛不住了,就需要對(duì)數(shù)據(jù)庫進(jìn)行分庫分表,但分庫分表后需要有一個(gè)唯一ID來標(biāo)識(shí)一條數(shù)據(jù),數(shù)據(jù)庫的自增ID顯然不能滿足需求;特別一點(diǎn)的如訂單、優(yōu)惠券也都需要有唯一ID做標(biāo)識(shí)。此時(shí)一個(gè)能夠生成全局唯一ID的系統(tǒng)是非常必要的。那么這個(gè)全局唯一ID就叫分布式ID。

2.分布式ID的特性

  • 唯一性:確保生成的ID是全網(wǎng)唯一的。
  • 有序遞增性:確保生成的ID是對(duì)于某個(gè)用戶或者業(yè)務(wù)是按一定的數(shù)字有序遞增的。
  • 高可用性:確保任何時(shí)候都能正確的生成ID。
  • 帶時(shí)間:ID里面包含時(shí)間,一眼掃過去就知道哪天的交易。

二、分布式ID的生成方案

1. UUID

算法的核心思想是結(jié)合機(jī)器的網(wǎng)卡、當(dāng)?shù)貢r(shí)間、一個(gè)隨記數(shù)來生成UUID。

優(yōu)點(diǎn):

  • 代碼實(shí)現(xiàn)簡單
  • 本地生成,沒有性能問題,沒有高可用風(fēng)險(xiǎn)
  • 全球唯一的,數(shù)據(jù)遷移容易

缺點(diǎn):

  • 長度過長,存儲(chǔ)冗余,且無序不可讀,查詢效率低
  • 每次生成的ID是無序的,不滿足趨勢(shì)遞增
  • UUID是字符串,而且比較長,占用空間大,查詢效率低
  • ID沒有含義,可讀性差

2. 數(shù)據(jù)庫自增ID

使用數(shù)據(jù)庫的id自增策略,如 MySQL 的 auto_increment。并且可以使用兩臺(tái)數(shù)據(jù)庫分別設(shè)置不同步長,生成不重復(fù)ID的策略來實(shí)現(xiàn)高可用。

  • 優(yōu)點(diǎn):數(shù)據(jù)庫生成的ID絕對(duì)有序,高可用實(shí)現(xiàn)方式簡單
  • 缺點(diǎn):需要獨(dú)立部署數(shù)據(jù)庫實(shí)例,成本高,有性能瓶頸

3. 批量生成ID

一次按需批量生成多個(gè)ID,每次生成都需要訪問數(shù)據(jù)庫,將數(shù)據(jù)庫修改為最大的ID值,并在內(nèi)存中記錄當(dāng)前值及最大值。

  • 優(yōu)點(diǎn):避免了每次生成ID都要訪問數(shù)據(jù)庫并帶來壓力,提高性能
  • 缺點(diǎn):屬于本地生成策略,存在單點(diǎn)故障,服務(wù)重啟造成ID不連續(xù)

4. Redis生成ID

Redis的所有命令操作都是單線程的,本身提供像 incr 和 increby 這樣的自增原子命令,所以能保證生成的 ID 肯定是唯一有序的。

  • 優(yōu)點(diǎn):有序遞增,可讀性強(qiáng),性能較高。不依賴于數(shù)據(jù)庫,靈活方便,且性能優(yōu)于數(shù)據(jù)庫;數(shù)字ID天然排序,對(duì)分頁或者需要排序的結(jié)果很有幫助。
  • 缺點(diǎn):占用帶寬,依賴Redis:如果系統(tǒng)中沒有Redis,還需要引入新的組件,增加系統(tǒng)復(fù)雜度;需要編碼和配置的工作量比較大。
    考慮到單節(jié)點(diǎn)的性能瓶頸,可以使用 Redis 集群來獲取更高的吞吐量。假如一個(gè)集群中有5臺(tái) Redis??梢猿跏蓟颗_(tái) Redis 的值分別是1, 2, 3, 4, 5,然后步長都是 5。各個(gè) Redis 生成的 ID 為:
A:1, 6, 11, 16, 21
B:2, 7, 12, 17, 22
C:3, 8, 13, 18, 23
D:4, 9, 14, 19, 24
E:5, 10, 15, 20, 25

隨便負(fù)載到哪個(gè)機(jī)確定好,未來很難做修改。步長和初始值一定需要事先確定。使用 Redis 集群也可以防止單點(diǎn)故障的問題。另外,比較適合使用 Redis 來生成每天從0開始的流水號(hào)。比如訂單號(hào) = 日期 + 當(dāng)日自增長號(hào)。可以每天在 Redis 中生成一個(gè) Key ,使用 INCR 進(jìn)行累加。

5. Twitter的snowflake算法

Twitter開源的snowflake,以?時(shí)間戳+機(jī)器+遞增序列?組成,基本趨勢(shì)遞增,且性能很高。

snowflake生成的是一個(gè)Long類型的值,Long類型的數(shù)據(jù)占用8個(gè) 字節(jié),也就是64位。SnowFlake將64進(jìn)行拆分,每個(gè)部分具有不同 的含義,當(dāng)然機(jī)器碼、序列號(hào)的位數(shù)可以自定義也可以。

把64-bit分別劃分成多段,分開來標(biāo)示機(jī)器、時(shí)間等,比如在snowflake中的64-bit分別表示如下圖(圖片來自網(wǎng)絡(luò))所示:

  • 符號(hào)位 (1bit):預(yù)留的符號(hào)位,恒為零。(由于 long 類型在 java 中帶符號(hào)的,最高位為符號(hào)位,正數(shù)為 0,負(fù)數(shù)為 1,且實(shí)際系統(tǒng)中所使用的ID一般都是正數(shù),所以最高位為 0)
  • 時(shí)間戳位 (41bit):41 位的時(shí)間戳可以容納的毫秒數(shù)是 2 的 41 次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000。通過計(jì)算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);結(jié)果約等于 69.73 年

ShardingSphere 的雪花算法的時(shí)間紀(jì)元從 2016 年 11 月 1 日零點(diǎn)開始,可以使用到 2086 年,相信能滿足絕大部分系統(tǒng)的要求。

  • 工作進(jìn)程位 (10bit):該標(biāo)志在 Java 進(jìn)程內(nèi)是唯一的,如果是分布式應(yīng)用部署應(yīng)保證每個(gè)工作進(jìn)程的 id 是不同的。該值默認(rèn)為 0,可通過屬性設(shè)置。10-bit機(jī)器可以分別表示1024臺(tái)機(jī)器。如果我們對(duì)IDC劃分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機(jī)器。這樣就可以表示32個(gè)IDC,每個(gè)IDC下可以有32臺(tái)機(jī)器,可以根據(jù)自身需求定義。
  • 序列號(hào)位 (12bit):該序列是用來在同一個(gè)毫秒內(nèi)生成不同的 ID。如果在這個(gè)毫秒內(nèi)生成的數(shù)量超過 4096 (2 的 12 次冪),那么生成器會(huì)等待到下個(gè)毫秒繼續(xù)生成。這 12 位計(jì)數(shù)支持每個(gè)節(jié)點(diǎn)每毫秒(同一臺(tái)機(jī)器,同一時(shí)刻)最多生成 1 << 12 = 4096個(gè)ID

優(yōu)點(diǎn):本地生成,不依賴中間件。 生成的分布式id足夠小,只有8個(gè)字節(jié),而且是遞增的

  • 能滿足高并發(fā)分布式系統(tǒng)環(huán)境下ID不重復(fù)
  • 生成效率高
  • 基于時(shí)間戳,可以保證基本有序遞增
  • 不依賴于第三方的庫或者中間件
  • 生成的id具有時(shí)序性和唯一性

缺點(diǎn):時(shí)鐘回?fù)軉栴},強(qiáng)烈依賴于服務(wù)器的時(shí)間,如果時(shí)間出現(xiàn)時(shí)間回?fù)?就可能出現(xiàn)重復(fù)的id

6. 百度UidGenerator

具體可以參考官網(wǎng)說明:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

7. 美團(tuán)Leaf

Leaf 是美團(tuán)開源的分布式ID生成器,能保證全局唯一性、趨勢(shì)遞增、單調(diào)遞增、信息安全,里面也提到了幾種分布式方案的對(duì)比,但也需要依賴關(guān)系數(shù)據(jù)庫、ZooKeeper等中間件。

https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

具體可以參考官網(wǎng)說明:https://tech.meituan.com/2017/04/21/mt-leaf.html

8.滴滴(Tinyid)

Tinyid由滴滴開發(fā),Github地址:https://github.com/didi/tinyid。
Tinyid是基于號(hào)段模式原理實(shí)現(xiàn)的與Leaf如出一轍,每個(gè)服務(wù)獲取一個(gè)號(hào)段(1000,2000]、(2000,3000]、(3000,4000]

在這里插入圖片描述

說了這么多,我們今天就主要說一下?Twitter開源的snowflake

三、snowflake

1.流程

2.java 代碼實(shí)現(xiàn)

?如下示例,41bit給時(shí)間戳,5bit給IDC,5bit給工作機(jī)器,12bit給序列號(hào),代碼中是寫死的,如果某些bit需要?jiǎng)討B(tài)調(diào)整,可在成員屬性定義。計(jì)算過程需要一些位運(yùn)算基礎(chǔ)。

import java.util.Date;/*** @Author allen* @Description TODO* @Date 2023-07-26 9:51* @Version 1.0*/
public class SnowFlakeUtil {private static SnowFlakeUtil snowFlakeUtil;static {snowFlakeUtil = new SnowFlakeUtil();}// 初始時(shí)間戳(紀(jì)年),可用雪花算法服務(wù)上線時(shí)間戳的值// 1650789964886:2022-04-24 16:45:59private static final long INIT_EPOCH = 1650789964886L;// 時(shí)間位取&private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;// 記錄最后使用的毫秒時(shí)間戳,主要用于判斷是否同一毫秒,以及用于服務(wù)器時(shí)鐘回?fù)芘袛鄍rivate long lastTimeMillis = -1L;// dataCenterId占用的位數(shù)private static final long DATA_CENTER_ID_BITS = 5L;// dataCenterId占用5個(gè)比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);// dataCenterIdprivate long dataCenterId;// workId占用的位數(shù)private static final long WORKER_ID_BITS = 5L;// workId占用5個(gè)比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);// workIdprivate long workerId;// 最后12位,代表每毫秒內(nèi)可產(chǎn)生最大序列號(hào),即 2^12 - 1 = 4095private static final long SEQUENCE_BITS = 12L;// 掩碼(最低12位為1,高位都為0),主要用于與自增后的序列號(hào)進(jìn)行位與,如果值為0,則代表自增后的序列號(hào)超過了4095// 0000000000000000000000000000000000000000000000000000111111111111private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 同一毫秒內(nèi)的最新序號(hào),最大值可為 2^12 - 1 = 4095private long sequence;// workId位需要左移的位數(shù) 12private static final long WORK_ID_SHIFT = SEQUENCE_BITS;// dataCenterId位需要左移的位數(shù) 12+5private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;// 時(shí)間戳需要左移的位數(shù) 12+5+5private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;/*** 無參構(gòu)造*/public SnowFlakeUtil() {this(1, 1);}/*** 有參構(gòu)造* @param dataCenterId* @param workerId*/public SnowFlakeUtil(long dataCenterId, long workerId) {// 檢查dataCenterId的合法值if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {throw new IllegalArgumentException(String.format("dataCenterId 值必須大于 0 并且小于 %d", MAX_DATA_CENTER_ID));}// 檢查workId的合法值if (workerId < 0 || workerId > MAX_WORKER_ID) {throw new IllegalArgumentException(String.format("workId 值必須大于 0 并且小于 %d", MAX_WORKER_ID));}this.workerId = workerId;this.dataCenterId = dataCenterId;}/*** 獲取唯一ID* @return*/public static Long getSnowFlakeId() {return snowFlakeUtil.nextId();}/*** 通過雪花算法生成下一個(gè)id,注意這里使用synchronized同步* @return 唯一id*/public synchronized long nextId() {long currentTimeMillis = System.currentTimeMillis();//System.out.println(currentTimeMillis);// 當(dāng)前時(shí)間小于上一次生成id使用的時(shí)間,可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉栴}if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉栴},請(qǐng)檢查服務(wù)器時(shí)間。當(dāng)前服務(wù)器時(shí)間戳:%d,上一次使用時(shí)間戳:%d", currentTimeMillis,lastTimeMillis));}if (currentTimeMillis == lastTimeMillis) {// 還是在同一毫秒內(nèi),則將序列號(hào)遞增1,序列號(hào)最大值為4095// 序列號(hào)的最大值是4095,使用掩碼(最低12位為1,高位都為0)進(jìn)行位與運(yùn)行后如果值為0,則自增后的序列號(hào)超過了4095// 那么就使用新的時(shí)間戳sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {currentTimeMillis = getNextMillis(lastTimeMillis);}} else { // 不在同一毫秒內(nèi),則序列號(hào)重新從0開始,序列號(hào)最大值為4095sequence = 0;}// 記錄最后一次使用的毫秒時(shí)間戳lastTimeMillis = currentTimeMillis;// 核心算法,將不同部分的數(shù)值移動(dòng)到指定的位置,然后進(jìn)行或運(yùn)行// <<:左移運(yùn)算符, 1 << 2 即將二進(jìn)制的 1 擴(kuò)大 2^2 倍// |:位或運(yùn)算符, 是把某兩個(gè)數(shù)中, 只要其中一個(gè)的某一位為1, 則結(jié)果的該位就為1// 優(yōu)先級(jí):<< > |return// 時(shí)間戳部分((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)// 數(shù)據(jù)中心部分| (dataCenterId << DATA_CENTER_ID_SHIFT)// 機(jī)器表示部分| (workerId << WORK_ID_SHIFT)// 序列號(hào)部分| sequence;}/*** 獲取指定時(shí)間戳的接下來的時(shí)間戳,也可以說是下一毫秒* @param lastTimeMillis 指定毫秒時(shí)間戳* @return 時(shí)間戳*/private long getNextMillis(long lastTimeMillis) {long currentTimeMillis = System.currentTimeMillis();while (currentTimeMillis <= lastTimeMillis) {currentTimeMillis = System.currentTimeMillis();}return currentTimeMillis;}/*** 獲取隨機(jī)字符串,length=13* @return*/public static String getRandomStr() {return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);}/*** 從ID中獲取時(shí)間* @param id 由此類生成的ID* @return*/public static Date getTimeBySnowFlakeId(long id) {return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);}public static void main(String[] args) {SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();long id = snowFlakeUtil.nextId();System.out.println(id);Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);System.out.println(date);long time = date.getTime();System.out.println(time);System.out.println(getRandomStr());/*        System.out.println("============================");long startTime = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {long id2 = snowFlakeUtil.nextId();System.out.println(id2);}System.out.println(System.currentTimeMillis() - startTime);*/}}

主要就這個(gè): long id = snowFlakeUtil.nextId();

?時(shí)鐘同步問題解決方案:

雪花算法-Java實(shí)現(xiàn)-解決時(shí)鐘回?fù)艿囊环N方法_雪花算法時(shí)鐘回?fù)躝fierys的博客-CSDN博客

這個(gè)代碼是借鑒另外一位博主的方案,可以借鑒一下,僅供參考

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;/**相較于標(biāo)準(zhǔn)算法,加入了時(shí)鐘回?fù)芙鉀Q方法,僅單機(jī)研究,僅個(gè)人思考,僅供參考*/
public class SnowFlow {//因?yàn)槎M(jìn)制里第一個(gè) bit 為如果是 1,那么都是負(fù)數(shù),但是我們生成的 id 都是正數(shù),所以第一個(gè) bit 統(tǒng)一都是 0。//機(jī)器ID  2進(jìn)制5位  32位減掉1位 31個(gè)private long workerId;//機(jī)房ID 2進(jìn)制5位  32位減掉1位 31個(gè)private long datacenterId;//代表一毫秒內(nèi)生成的多個(gè)id的最新序號(hào)  12位 4096 -1 = 4095 個(gè)private long sequence;//設(shè)置一個(gè)時(shí)間初始值    2^41 - 1   差不多可以用69年private long twepoch = 1420041600000L;//5位的機(jī)器idprivate long workerIdBits = 5L;//5位的機(jī)房id;?!畃rivate long datacenterIdBits = 5L;//每毫秒內(nèi)產(chǎn)生的id數(shù) 2 的 12次方private long sequenceBits = 12L;// 這個(gè)是二進(jìn)制運(yùn)算,就是5 bit最多只能有31個(gè)數(shù)字,也就是說機(jī)器id最多只能是32以內(nèi)private long maxWorkerId = -1L ^ (-1L << workerIdBits);// 這個(gè)是一個(gè)意思,就是5 bit最多只能有31個(gè)數(shù)字,機(jī)房id最多只能是32以內(nèi)private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);private long workerIdShift = sequenceBits;private long datacenterIdShift = sequenceBits + workerIdBits;private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;// -1L 二進(jìn)制就是1111 1111  為什么?// -1 左移12位就是 1111  1111 0000 0000 0000 0000// 異或  相同為0 ,不同為1// 1111  1111  0000  0000  0000  0000// ^// 1111  1111  1111  1111  1111  1111// 0000 0000 1111 1111 1111 1111 換算成10進(jìn)制就是4095private long sequenceMask = -1L ^ (-1L << sequenceBits);//記錄產(chǎn)生時(shí)間毫秒數(shù),判斷是否是同1毫秒private long lastTimestamp = -1L;public long getWorkerId(){return workerId;}public long getDatacenterId() {return datacenterId;}public long getTimestamp() {return System.currentTimeMillis();}//是否發(fā)生了時(shí)鐘回?fù)躳rivate boolean isBackwordsFlag = false;//是否是第一次發(fā)生時(shí)鐘回?fù)? 用于設(shè)置時(shí)鐘回?fù)軙r(shí)間點(diǎn)private boolean isFirstBackwordsFlag = true;//記錄時(shí)鐘回?fù)馨l(fā)生時(shí)間點(diǎn), 用于判斷回?fù)芎蟮臅r(shí)間達(dá)到回?fù)軙r(shí)間點(diǎn)時(shí), 跳過 已經(jīng)用過的 時(shí)鐘回?fù)馨l(fā)生時(shí)間點(diǎn) 之后的時(shí)間 到 未來時(shí)間的當(dāng)前時(shí)間點(diǎn)private long backBaseTimestamp = -1L;public SnowFlow() {}public SnowFlow(long workerId, long datacenterId, long sequence) {// 檢查機(jī)房id和機(jī)器id是否超過31 不能小于0if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;this.sequence = sequence;}// 這個(gè)是核心方法,通過調(diào)用nextId()方法,// 讓當(dāng)前這臺(tái)機(jī)器上的snowflake算法程序生成一個(gè)全局唯一的idpublic synchronized long nextId() {// 這兒就是獲取當(dāng)前時(shí)間戳,單位是毫秒long timestamp = timeGen();//--20220813--1---------------------------------------if (isBackwordsFlag) {//當(dāng)回?fù)軙r(shí)間再次叨叨回?fù)軙r(shí)間點(diǎn)時(shí), 跳過回?fù)苓@段時(shí)間里已經(jīng)使用了的未來時(shí)間if (timestamp >= backBaseTimestamp && timestamp < lastTimestamp) {//直接將當(dāng)前時(shí)間設(shè)置為最新的未來時(shí)間timestamp = lastTimestamp;} else if(timestamp > lastTimestamp) {//當(dāng)前時(shí)間已經(jīng)大于上次時(shí)間, 重置時(shí)鐘回?fù)軜?biāo)志isBackwordsFlag = false;isFirstBackwordsFlag = true;System.out.println("時(shí)間已恢復(fù)正常-->" + timestamp);} else {// timestamp == lastTimestamp 等于的情況在后面}}//--20220813--1----------------------------------------// 判斷是否小于上次時(shí)間戳,如果小于的話,就拋出異常if (timestamp < lastTimestamp) {System.err.printf("lastTimestamp=%d, timestamp=%d, l-t=%d \n", lastTimestamp, timestamp, lastTimestamp - timestamp);
//            throw new RuntimeException(
//                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
//                            lastTimestamp - timestamp));//--20220813--2---------------------------------------//這里不再拋出異常, 改為記錄時(shí)鐘回?fù)馨l(fā)生時(shí)間點(diǎn)//發(fā)生時(shí)鐘回?fù)芎? 當(dāng)前時(shí)間 timestamp 就變成了 過去的時(shí)間//此時(shí)將 timestamp 設(shè)置為 上一次時(shí)間, 即相對(duì)于當(dāng)前時(shí)間的未來時(shí)間timestamp = lastTimestamp;isBackwordsFlag = true;//記錄時(shí)鐘回?fù)馨l(fā)生的時(shí)間點(diǎn), 后續(xù)需要跳過已經(jīng)使用的未來時(shí)間段if (isFirstBackwordsFlag) {backBaseTimestamp = timestamp;isFirstBackwordsFlag = false;System.out.println("時(shí)鐘回?fù)芤寻l(fā)生-->" + backBaseTimestamp);}//--20220813--2---------------------------------------}// 下面是說假設(shè)在同一個(gè)毫秒內(nèi),又發(fā)送了一個(gè)請(qǐng)求生成一個(gè)id// 這個(gè)時(shí)候就得把seqence序號(hào)給遞增1,最多就是4096if (timestamp == lastTimestamp) {// 這個(gè)意思是說一個(gè)毫秒內(nèi)最多只能有4096個(gè)數(shù)字,無論你傳遞多少進(jìn)來,//這個(gè)位運(yùn)算保證始終就是在4096這個(gè)范圍內(nèi),避免你自己傳遞個(gè)sequence超過了4096這個(gè)范圍sequence = (sequence + 1) & sequenceMask;//當(dāng)某一毫秒的時(shí)間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會(huì)進(jìn)入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生IDif (sequence == 0) {//timestamp = tilNextMillis(lastTimestamp);//--20220813--3---------------------------------------//這里也不能阻塞了, 因?yàn)樽枞椒ㄖ行枰玫疆?dāng)前時(shí)間, 改為將此時(shí)代表未來的時(shí)間 加 1if (isBackwordsFlag) {lastTimestamp++;//根據(jù)博友評(píng)論反饋, 這里可能需要重新賦值, 如果有人看到這個(gè), 可以驗(yàn)證//timestamp = lastTimestamp++;} else {timestamp = tilNextMillis(lastTimestamp);}//--20220813--3---------------------------------------}} else {//sequence = 0;//每毫秒的序列號(hào)都從0開始的話,會(huì)導(dǎo)致沒有競爭情況返回的都是偶數(shù)。解決方法是用時(shí)間戳&1,這樣就會(huì)隨機(jī)得到1或者0。sequence = timestamp & 1;}// 這兒記錄一下最近一次生成id的時(shí)間戳,單位是毫秒//lastTimestamp = timestamp;//--20220813--4---------------------------------------if(isBackwordsFlag) {//什么都不做} else {lastTimestamp = timestamp;}//--20220813--4---------------------------------------// 這兒就是最核心的二進(jìn)制位運(yùn)算操作,生成一個(gè)64bit的id// 先將當(dāng)前時(shí)間戳左移,放到41 bit那兒;將機(jī)房id左移放到5 bit那兒;將機(jī)器id左移放到5 bit那兒;將序號(hào)放最后12 bit// 最后拼接起來成一個(gè)64 bit的二進(jìn)制數(shù)字,轉(zhuǎn)換成10進(jìn)制就是個(gè)long型long sn = ((timestamp - twepoch) << timestampLeftShift) |(datacenterId << datacenterIdShift) |(workerId << workerIdShift) | sequence;if (isBackwordsFlag) {System.out.printf("sn=%d\n", sn);}return sn;}/*** 當(dāng)某一毫秒的時(shí)間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會(huì)進(jìn)入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生ID* @param lastTimestamp* @return*/private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}//獲取當(dāng)前時(shí)間戳private long timeGen(){return System.currentTimeMillis();}/***  main 測(cè)試類* @param args*/public static void main(String[] args) throws IOException, InterruptedException {SnowFlow snowFlow = new SnowFlow(1, 1, 1);int count = 10000000;//int count = 100;for (int i = 0; i < count; i++) {//實(shí)際測(cè)試發(fā)現(xiàn)遍歷太快輸出日志過多導(dǎo)致卡頓, 增加睡眠時(shí)間, 或輸出到文件snowFlow.nextId();//Thread.sleep(100);//            System.out.println(snowFlow.nextId());//            if (i == 1000) {//不具有管理員權(quán)限的用戶, 修改不成功//testClockMvoedBackwords(30);
//            }//改為 手動(dòng)修改,  右鍵cmd,以管理員權(quán)限打開后,使用time命令即可, time 16:15:00}System.out.println(System.currentTimeMillis());/*** 這里為什么特意輸出一個(gè)開始時(shí)間呢, 其實(shí)就是一個(gè)運(yùn)行了兩年的程序突然有一天出bug了,導(dǎo)致了嚴(yán)重的生產(chǎn)事件!* 那么時(shí)間初始化影響什么呢, 答案是 序列的長度* 有人就說了, 這個(gè)一般是作為 主鍵用的, 長度貌似影響不大, 確實(shí)是這樣的* 這次的bug不是雪花算法本身的問題, 而是程序里面有個(gè)功能是嚴(yán)格長度截取的, 并且只考慮了長度不夠的情況, 沒有考慮到變長的情況* 最根本的原因是 本人截取的時(shí)候 序列的長度一直是18位, 然后截取9位的代碼是這么寫的 substring(9);* 當(dāng)未來的某一天序列長度增加到了19位,那么這個(gè)截取就會(huì)返回10位長度, 最終導(dǎo)致一個(gè)大范圍的交易失敗......* 鍋當(dāng)然是本人背, 這里提出這種情況, 供大家參考.* 經(jīng)過仔細(xì)研究所謂的序列可以使用69年, 序列的長度變化是這樣的, 假設(shè)以當(dāng)前時(shí)間為初始化值* 12 13 14 15 16 17 18(約7年) 19(約58年)* 長度隨時(shí)間增加, 長度越長, 保持相同長度的時(shí)間越長*/DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String dateString = "2015-01-01 00:00:00";LocalDateTime localDateTime = LocalDateTime.parse(dateString,dtf2);System.out.println(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());}//windows os 模擬時(shí)鐘回?fù)? 將當(dāng)前時(shí)間減去幾秒private static void testClockMvoedBackwords(long seconds) throws IOException {System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));LocalDateTime localDateTime = LocalDateTime.now();String backTime = localDateTime.minusSeconds(seconds).format(DateTimeFormatter.ofPattern("HH:mm:ss"));System.out.println(backTime);if (System.getProperty("os.name").contains("Windows")) {String cmd = "cmd /c start time 15:41:56";// + backTime;//不具有管理員權(quán)限的用戶, 修改不生效, 提示 客戶端無所需特權(quán)Runtime.getRuntime().exec(cmd);
//            Runtime.getRuntime().exec("cmd /c notepad");System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));}}
}

3.Snowflake生產(chǎn)方案 時(shí)鐘回?fù)軉栴}解決思路

第一種辦法,就是關(guān)閉時(shí)鐘同步,避免產(chǎn)生時(shí)鐘同步問題,不過這個(gè)不太現(xiàn)實(shí),因?yàn)閺?qiáng)依賴時(shí)間的系統(tǒng),一般都得做時(shí)鐘同步,避免時(shí)間嚴(yán)重錯(cuò)誤,在虛擬機(jī)上部署一些東西,玩兒虛擬機(jī),休眠,再次恢復(fù)之后往往虛擬機(jī)里的時(shí)間和宿主機(jī)的時(shí)間是不同步的導(dǎo)致一些大數(shù)據(jù)的分布式系統(tǒng)會(huì)崩潰掉,節(jié)點(diǎn)之間通信會(huì)依賴時(shí)間戳進(jìn)行比對(duì),心跳過慢,就會(huì)導(dǎo)致節(jié)點(diǎn)掛掉


第二種辦法,記錄下來上一次生成ID的時(shí)間,如果發(fā)現(xiàn)本次生成ID的時(shí)候,時(shí)間戳小于上次的時(shí)間戳,說明時(shí)鐘回?fù)芰?#xff0c;此時(shí)就這個(gè)時(shí)間內(nèi)不允許生成ID,一直等,等待到當(dāng)前時(shí)間追上上一次生成時(shí)間,問題在于,萬一回?fù)艿臅r(shí)間太多了呢?可能要等很久,影響了系統(tǒng)的可用性,所以也不是特別好的辦法內(nèi)存里可以存上一次生成唯一ID的時(shí)間戳,時(shí)鐘回?fù)芰?#xff0c;把當(dāng)前時(shí)間戳?xí)負(fù)艿缴洗螘r(shí)間戳之前,請(qǐng)求過來,要生成唯一ID,你不要直接就返回一個(gè)ID給他,你先做一個(gè)比較,如果你發(fā)現(xiàn)當(dāng)前時(shí)間戳跟上一次生成唯一ID的時(shí)間戳想比,比他小判定,時(shí)鐘回?fù)?#xff0c;只要你生成ID,就有可能會(huì)發(fā)生ID的重復(fù)可用性這么差的話,人家業(yè)務(wù)服務(wù)萬一此時(shí)是要生成一個(gè)賬單數(shù)據(jù),申請(qǐng)一個(gè)ID,此時(shí)你好不容易等待了幾百毫秒之后,你還告訴他你內(nèi)部異常,沒法獲取到唯一ID,反復(fù)的重試,你會(huì)影響他的業(yè)務(wù)服務(wù)的運(yùn)行。
?

第三種辦法,針對(duì)第二種辦法的優(yōu)化,如果發(fā)現(xiàn)時(shí)鐘回?fù)芴萘?#xff0c;比如超過了1分鐘,此時(shí)直接就報(bào)警,同時(shí)不再對(duì)外提供服務(wù),把自己從集群里摘了,比如你要是基于微服務(wù)注冊(cè)中心進(jìn)行注冊(cè)的,就得主動(dòng)做一個(gè)下線當(dāng)你發(fā)現(xiàn)當(dāng)前時(shí)間戳比上一次生成ID的時(shí)間戳要小,發(fā)現(xiàn)時(shí)鐘回?fù)芰?#xff0c;判斷一下,回?fù)芰硕嗌俸撩?#xff0c;比如說回?fù)軙r(shí)間在500ms以內(nèi),此時(shí)可以hang住請(qǐng)求,等待500ms,等到500ms之后,當(dāng)前時(shí)間戳比上一次生成ID的時(shí)間戳要大了
此時(shí)就可以正常生成唯一ID返回給業(yè)務(wù)方了,對(duì)于業(yè)務(wù)方而言,僅僅是在個(gè)別少數(shù)的時(shí)鐘回?fù)艿那闆r之下,請(qǐng)求平時(shí)只要50ms,500ms,還在接受范圍之內(nèi),所以說還是可以的,只不過請(qǐng)求慢了一些
如果你要是發(fā)現(xiàn)你當(dāng)前時(shí)間戳和上一次生成唯一ID的時(shí)間戳想比,你一比較,就發(fā)現(xiàn)超過了500ms了,超過了500ms了,但是在5s之內(nèi),此時(shí)你可以返回一個(gè)異常狀態(tài)+異常持續(xù)時(shí)間給客戶端,不要說有問題,可以通知他自行進(jìn)行重試
重試機(jī)制,最好不要讓業(yè)務(wù)方自己去做,你完全可以去封裝一個(gè)你的唯一ID生成服務(wù)的客戶端,基于RPC請(qǐng)求你的接口,但是你在自己的客戶端里封裝一個(gè)自動(dòng)重試的機(jī)制,他一旦發(fā)現(xiàn)某臺(tái)服務(wù)器返回的響應(yīng)說自己短時(shí)間內(nèi)沒法提供服務(wù),他自動(dòng)就去請(qǐng)求其他機(jī)器上的服務(wù)獲取唯一ID了
如果要解決時(shí)鐘回?fù)?#xff0c;一般是第二種和第三種結(jié)合在一起來用,但是被動(dòng)等待甚至主動(dòng)下線,總是影響系統(tǒng)可用性的,都不是特別好
服務(wù)端的時(shí)鐘回?fù)軝z測(cè)機(jī)制 + 客戶端自己封裝
1s以內(nèi):阻塞請(qǐng)求等待,客戶端的超時(shí)時(shí)間,應(yīng)該也是1s,暴露1s內(nèi)每一毫秒生成過的唯一ID最大的序號(hào),根據(jù)當(dāng)前時(shí)間戳的毫秒,定位到之前生成過ID的這一毫秒的最大ID序號(hào),此時(shí)繼續(xù)生成ID,直接在之前生成過的這一毫秒的最大ID序號(hào)基礎(chǔ)上遞增就可以了,優(yōu)化之后,就可以保證不需要阻塞等待
1s~10s之間:返回異常碼和異常持續(xù)時(shí)間,客戶端在指定時(shí)間內(nèi)不請(qǐng)求這臺(tái)機(jī)器
10s以上:返回故障碼,請(qǐng)求服務(wù)注冊(cè)中心讓自己下線,客戶端收到故障碼之后,就直接把這個(gè)機(jī)器從服務(wù)機(jī)器列表里剔除掉,不請(qǐng)求他了,后續(xù)等到那臺(tái)機(jī)器部署的ID服務(wù)他發(fā)現(xiàn)自己的時(shí)間可能過了幾秒鐘,緩過來了,恢復(fù)了,可用了,就可以再次進(jìn)行服務(wù)注冊(cè),你客戶端刷新服務(wù)注冊(cè)列表的時(shí)候,就會(huì)發(fā)現(xiàn)他,此時(shí)可以再次去請(qǐng)求他
?

第四種辦法,要在內(nèi)存里維護(hù)最近幾秒內(nèi)生成的ID值,一般時(shí)鐘回?fù)芏际菐资撩氲綆装俸撩?#xff0c;很少會(huì)超過秒的,所以保存最近幾秒的就行了,然后如果發(fā)生了時(shí)鐘回?fù)?#xff0c;此時(shí)就看看回?fù)艿搅四囊缓撩?#xff0c;因?yàn)闀r(shí)間戳是毫秒級(jí)的,接著就看那一毫秒
從那一毫秒生產(chǎn)過的ID序號(hào)往后繼續(xù)生成就可以了,后續(xù)每一毫秒都是依次類推,這樣就可以完美避免重復(fù)問題,還不用等待
但是這里也得做一個(gè)兜底機(jī)制,就是比如你保留最近10s內(nèi)每一毫秒生成的ID,那么萬一時(shí)鐘回?fù)芘銮沙^了10s呢?此時(shí)這種概率很低,你可以跟二三兩個(gè)方案結(jié)合,設(shè)置幾個(gè)閾值,比如說,你保留最近10s的ID,回?fù)?0s內(nèi)都可以保證不重復(fù),不停頓;如果超過10s,在60s內(nèi),可以有一個(gè)等待的過程,讓他時(shí)間前進(jìn)到你之前保留過的10s范圍內(nèi)去;如果回?fù)艹^了60s,直接下線
上一次生成唯一ID的時(shí)間戳也沒了,最近1s內(nèi)每一毫秒的最大ID序號(hào)也沒了,重啟之后,出現(xiàn)了時(shí)間回?fù)?#xff0c;發(fā)現(xiàn)不了時(shí)間回?fù)軉栴},其次也沒有辦法繼續(xù)按照之前的思路去生成不重復(fù)的唯一ID了

4.時(shí)鐘回?fù)軆?yōu)化

1、我們一般需要打開時(shí)鐘同步功能,這樣ID才能夠最大化的保證按照時(shí)間有序,但是時(shí)鐘同步打開后,就可能會(huì)時(shí)鐘回?fù)芰?#xff0c;如果時(shí)鐘回?fù)芰?#xff0c;那么生成的ID就會(huì)重復(fù),為此我們一般打開時(shí)鐘同步的同時(shí)關(guān)閉時(shí)鐘回?fù)芄δ?#xff1b;
2、序列號(hào)的位數(shù)有限,能表示的ID個(gè)數(shù)有限,時(shí)鐘同步的時(shí)候,如果某臺(tái)服務(wù)器快了很多,雖然關(guān)閉了時(shí)鐘回?fù)?#xff0c;但是在時(shí)間追趕上前,ID可能已經(jīng)用完,當(dāng)自增序列號(hào)用完了,我們可以做如下的工作:停止ID生成服務(wù)并告警、如果時(shí)鐘回?fù)苄∮谝欢ǖ拈撝祫t等待、如大于一定的閾值則通過第三方組件如ZK重新生成一個(gè)workerid或者自增時(shí)間戳借用下一個(gè)時(shí)間戳的ID;
3、服務(wù)重啟后,ID可能會(huì)重復(fù),為此我們一般需要定期保存時(shí)間戳,重啟后的時(shí)間戳必須大于保存的時(shí)間戳+幾倍保存間隔時(shí)間(如3倍),為什么要幾倍呢,主要是考慮到數(shù)據(jù)丟失的情況,但是如果保存到本地硬盤且每次保存都fsync,此時(shí)1倍即可。重啟后如果小于可以像第二點(diǎn)那樣類似處理;
4、如果請(qǐng)求ID的QPS不高,比如每毫秒一個(gè),那么每次獲取的ID的尾號(hào)都是0,那么基于ID做分庫分表,可能數(shù)據(jù)分布就會(huì)不均,此時(shí)我們可以增加時(shí)間戳的時(shí)間間隔或者序列號(hào)每次從隨機(jī)一個(gè)值開始自增。

主要是生成id的這個(gè)方法,我把這個(gè)再優(yōu)化一下,這個(gè)是根據(jù)美團(tuán)leaf做了進(jìn)一步優(yōu)化,請(qǐng)參考

    /*** 通過雪花算法生成下一個(gè)id,注意這里使用synchronized同步* @return 唯一id*/public synchronized long nextId() {long currentTimeMillis = System.currentTimeMillis();//System.out.println(currentTimeMillis);// 當(dāng)前時(shí)間小于上一次生成id使用的時(shí)間,可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉栴}//long timestamp = timeGen();if (currentTimeMillis < lastTimeMillis) {long offset = lastTimeMillis - currentTimeMillis;if (offset <= 5) {try {wait(offset << 1);currentTimeMillis = timeGen();if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException("id生成失敗");}} catch (InterruptedException e) {throw new RuntimeException("生成id時(shí)出現(xiàn)錯(cuò)誤");}} else {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉栴},請(qǐng)檢查服務(wù)器時(shí)間。當(dāng)前服務(wù)器時(shí)間戳:%d,上一次使用時(shí)間戳:%d", currentTimeMillis,lastTimeMillis));}}/*if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出現(xiàn)服務(wù)器時(shí)鐘回?fù)軉栴},請(qǐng)檢查服務(wù)器時(shí)間。當(dāng)前服務(wù)器時(shí)間戳:%d,上一次使用時(shí)間戳:%d", currentTimeMillis,lastTimeMillis));}*/if (currentTimeMillis == lastTimeMillis) {// 還是在同一毫秒內(nèi),則將序列號(hào)遞增1,序列號(hào)最大值為4095// 序列號(hào)的最大值是4095,使用掩碼(最低12位為1,高位都為0)進(jìn)行位與運(yùn)行后如果值為0,則自增后的序列號(hào)超過了4095// 那么就使用新的時(shí)間戳sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {currentTimeMillis = getNextMillis(lastTimeMillis);}} else { // 不在同一毫秒內(nèi),則序列號(hào)重新從0開始,序列號(hào)最大值為4095sequence = 0;}// 記錄最后一次使用的毫秒時(shí)間戳lastTimeMillis = currentTimeMillis;// 核心算法,將不同部分的數(shù)值移動(dòng)到指定的位置,然后進(jìn)行或運(yùn)行// <<:左移運(yùn)算符, 1 << 2 即將二進(jìn)制的 1 擴(kuò)大 2^2 倍// |:位或運(yùn)算符, 是把某兩個(gè)數(shù)中, 只要其中一個(gè)的某一位為1, 則結(jié)果的該位就為1// 優(yōu)先級(jí):<< > |return// 時(shí)間戳部分((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)// 數(shù)據(jù)中心部分| (dataCenterId << DATA_CENTER_ID_SHIFT)// 機(jī)器表示部分| (workerId << WORK_ID_SHIFT)// 序列號(hào)部分| sequence;}

【分布式】分布式唯一 ID 的 8 種生成方案

SnowFlake 雪花算法詳解與實(shí)現(xiàn) - 掘金

雪花算法(SnowFlake)_文丑顏不良啊的博客-CSDN博客

分布式唯一Id(雪花算法),原理+對(duì)比+方案 - 簡書

雪花算法snowflake分布式id生成原理詳解,以及對(duì)解決時(shí)鐘回?fù)軉栴}幾種方案討論_51CTO博客_snowflake 分布式id

Leaf——美團(tuán)點(diǎn)評(píng)分布式ID生成系統(tǒng) - 美團(tuán)技術(shù)團(tuán)隊(duì)

?https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

Snowflake生產(chǎn)方案 時(shí)鐘回?fù)軉栴}解決思路_啟動(dòng)報(bào)錯(cuò) snowflake 時(shí)鐘回?fù)芙鉀Q方法_都是底層的博客-CSDN博客

http://m.aloenet.com.cn/news/29265.html

相關(guān)文章:

  • 怎么創(chuàng)網(wǎng)站賺錢嗎微信朋友圈廣告推廣代理
  • 怎么看深圳網(wǎng)站頁面設(shè)計(jì)寫一篇軟文推廣自己的學(xué)校
  • 歐泰國際物流網(wǎng)站市場(chǎng)營銷策略有哪些
  • 做網(wǎng)站建設(shè)推薦東莞百度網(wǎng)站排名優(yōu)化
  • 地產(chǎn)網(wǎng)站設(shè)計(jì)成都關(guān)鍵詞seo推廣平臺(tái)
  • 寧波seo快速優(yōu)化怎么做淘寶怎么優(yōu)化關(guān)鍵詞排名
  • 西安網(wǎng)站建設(shè)價(jià)格明細(xì)seo入門到精通
  • 微信公眾號(hào)直接鏈接網(wǎng)站怎么做長春網(wǎng)站seo
  • 游戲直播網(wǎng)站怎么做百度信息流開戶多少錢
  • 石家莊電子商務(wù)網(wǎng)站建設(shè)焦作網(wǎng)站seo
  • 旺蒼網(wǎng)站建設(shè)seo入門培訓(xùn)學(xué)校
  • 南陽網(wǎng)站建設(shè)公司湖南官網(wǎng)網(wǎng)站推廣軟件
  • 網(wǎng)站驗(yàn)證碼怎么做steam交易鏈接在哪
  • 溧陽做網(wǎng)站哪家好零基礎(chǔ)學(xué)什么技術(shù)好
  • html5畢業(yè)設(shè)計(jì)題目大全福州百度推廣排名優(yōu)化
  • 信用門戶網(wǎng)站建設(shè)專家評(píng)價(jià)免費(fèi)網(wǎng)站收錄網(wǎng)站推廣
  • 一個(gè)域名權(quán)重3如果做網(wǎng)站的話權(quán)重會(huì)降為0嗎百度熱度榜搜索趨勢(shì)
  • 網(wǎng)站建設(shè)平臺(tái)策劃電腦優(yōu)化是什么意思
  • 陜西省建設(shè)銀行網(wǎng)站6seo如何優(yōu)化
  • ppt歡迎頁面模板網(wǎng)絡(luò)營銷中的seo與sem
  • 不在百度做推廣他會(huì)把你的網(wǎng)站排名弄掉怎么可以讓百度快速收錄視頻
  • 昆明網(wǎng)站建設(shè)-中國互聯(lián)精準(zhǔn)客戶數(shù)據(jù)采集軟件
  • 小學(xué)網(wǎng)站源碼php個(gè)人在線做網(wǎng)站免費(fèi)
  • 做網(wǎng)站話掙錢嗎google下載官網(wǎng)
  • 武平縣網(wǎng)站建設(shè)搜索引擎競價(jià)廣告
  • seo優(yōu)化價(jià)格seo1搬到哪里去了
  • 樂清網(wǎng)站設(shè)計(jì)公司哪家好百度 營銷中心
  • 哈爾濱自助建站模板網(wǎng)站推廣軟件
  • 萊蕪金點(diǎn)子信息港最新招聘信息港南寧seo優(yōu)化公司
  • 山東濟(jì)南網(wǎng)站建設(shè)公司深圳公司網(wǎng)絡(luò)推廣該怎么做