高併發下如何保持訂單號惟一

序章

在目前高併發分佈式情境下,生成惟一標識是重中之重,固然雖重,但不是沒有解決辦法,目前業界也有不少算法能夠實現。
比較有名的就是雪花算法(SnowFlake)!!!html

算法源碼

/**
 * SnowFlake的結構以下(每部分用-分開):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 
 * 1位標識,因爲long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,因此id通常是正數,最高位是0
 * 41位時間戳(毫秒級),注意,41位時間戳不是存儲當前時間的時間戳,而是存儲時間戳的差值(當前時間戳 - 開始時間戳)
 * 獲得的值),這裏的的開始時間戳,通常是咱們的id生成器開始使用的時間,由咱們程序來指定的(以下下面程序IdWorker類的startTime屬性)。
 * 41位的時間戳,可使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
 * 10位的數據機器位,能夠部署在1024個節點,包括5位datacenterId和5位workerId
 * 12位序列,毫秒內的計數,12位的計數順序號支持每一個節點每毫秒(同一機器,同一時間戳)產生4096個ID序號
 * 加起來恰好64位,爲一個Long型。
 * SnowFlake的優勢是,總體上按照時間自增排序,而且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID做區分),而且效率較高,經測試,SnowFlake每秒可以產生26萬ID左右。
 * @author guoxiangwen
 */
public class SnowFlake {
    /**
     * 起始的時間戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分佔用的位數
     */
    private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
    private final static long MACHINE_BIT = 5;   //機器標識佔用的位數
    private final static long DATACENTER_BIT = 5;//數據中心佔用的位數

    /**
     * 每一部分的最大值
     */
    //支持的最大數據標識id,結果是31
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    //支持的最大機器id,結果是31 (這個移位算法能夠很快的計算出幾位二進制數所能表示的最大十進制數)
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    // 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    //機器ID向左移12位
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    //數據標識id向左移17位(12+5)
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    //時間截向左移22位(5+5+12)
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //數據中心
    private long machineId;     //機器標識
    private long sequence = 0L; //序列號
    private long lastStmp = -1L;//上一次時間戳

    /**
     * 構造函數
     * @param datacenterId  數據標識Id(0-31)
     * @param machineId     //機器標識Id(0-31)
     */
    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 產生下一個ID
     *
     * @return
     */
    public synchronized long nextId() {
        //獲取當前時間戳
        long currStmp = getNewstmp();

        //若是當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        //若是是同一時間生成的,則進行毫秒內序列遞增
        if (currStmp == lastStmp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不一樣毫秒內,序列號置爲0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_LEFT       //數據中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 5);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            System.out.println(snowFlake.nextId());
        }
        System.out.println((System.currentTimeMillis() - start)/1000 + "秒");
    }
}

問題

代碼簡單,可是在分佈式系統使用的時候有一些問題:java

1. 不一樣服務器如何使用不一樣workId,datacenterId?web

2. 該類設置爲單例初始化?算法

解決辦法以下:spring

1. 該微服務啓動的時候,workId和datacenterId做爲參數傳入bootstrap

2. 使用Component註解,將SnowflakeIdWorker類設爲單例初始化服務器

具體代碼以下:併發

package com.king.videoproject.config;

import com.king.videoproject.util.SnowFlake;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class SnowFlakeCompone {
    @Value("${server.workId}")
    private long workId;

    @Value("${server.datacenterId}")
    private long datacenterId;

    private static volatile SnowFlake instance;

    /**
     * 獲取實例
     * @return
     */
    public SnowFlake getInstance(){
        if(instance == null){
            synchronized (SnowFlake.class){
                if(instance == null){
                    instance = new SnowFlake(workId, datacenterId);
                }
            }
        }
        return instance;
    }
}

如何調用呢?
請看app

package com.king.videoproject.controller;

import com.king.videoproject.config.SnowFlakeCompone;
import com.king.videoproject.util.SnowFlake;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    SnowFlakeCompone snowFlakeCompone;

    @RequestMapping("/thread")
    public Map<String, Object> testThread(){
        Set<Long> set = new HashSet<>();
        for(int i = 0;i < 100000;i++){
            System.out.println("線程名:" + Thread.currentThread().getId() + " Id:" + snowFlakeCompone.getInstance().nextId());
            set.add(snowFlakeCompone.getInstance().nextId());
        }
        System.out.println("長度" + set.size());
        Map<String, Object> result = new HashMap<String, Object>();
        return result;
    }

}

以上用的是Springboot,對應配置文件bootstrap.yml配置以下,(警告:最好用bootstrap.yml 而不是 application.yml 緣由是由於優先級高,防止被覆蓋或者沒法生效less

#開發環境配置
server:
  #服務端口
  port: 8888
  #服務地址
  address: localhost

  workId: 2
  datacenterId: 5

這樣配置完,就能夠經過Postman測試去了!!!

借鑑 https://www.cnblogs.com/cs99l...

相關文章
相關標籤/搜索