Боевая серия Java Spike System ~ распределенный номер заказа генерации уникального идентификатора

распределенный

Резюме:

Это сообщение в блоге является седьмой статьей из серии "Практические боевые действия системы Java Seckill". мы вводим два метода, один из которых представляет собой традиционный способ генерации случайных чисел, а другой представляет собой реализацию популярного в настоящее время «распределенного алгоритма генерации уникальных идентификаторов — алгоритма снежинки».

содержание:

В последнем сообщении блога мы завершили битву кода бизнес-логики продукта seckill, В этом коде мы также реализовали «когда у пользователя есть успех seckill, необходимо создать запись заказа для успеха seckill в таблица базы данных». соответствующий код выглядит следующим образом:

//通用的方法-记录用户秒杀成功后生成的订单-并进行异步邮件消息的通知
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
    //TODO:记录抢购成功后生成的秒杀订单记录
 
    ItemKillSuccess entity=new ItemKillSuccess();
    
    //此处为订单编号的生成逻辑
String orderNo=String.valueOf(snowFlake.nextId());
    //entity.setCode(RandomUtil.generateOrderCode());   //传统时间戳+N位随机数
entity.setCode(orderNo); //雪花算法
 
    entity.setItemId(kill.getItemId());
    entity.setKillId(kill.getId());
    entity.setUserId(userId.toString());
    entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
    entity.setCreateTime(DateTime.now().toDate());
    //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法
    if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);
 
        //其他逻辑省略
    }
}

В этой логике реализации ключевым моментом является «как эффективно генерировать порядковые номера в среде с высокой степенью параллелизма», так насколько это эффективно? Debug считает, что должны быть выполнены следующие два пункта:

(1) Убедитесь, что логика генерации номера заказа работает быстро и стабильно, и уменьшите задержку

(2) Необходимо обеспечить, чтобы сгенерированный номер заказа был уникальным в глобальном масштабе, неповторяющимся, возрастающим по тренду и последовательным во времени.

Ниже мы используем два метода для генерации «номера заказа» и пишем многопоточную программу для моделирования того, соответствует ли сгенерированный номер заказа условиям.

Стоит отметить, что для интуитивного наблюдения за тем, является ли номер заказа, сгенерированный одновременно с помощью многопоточности, уникальным и имеет тенденцию к увеличению, Debug использует таблицу базы данных random_code для хранения сгенерированного номера заказа, а его DDL выглядит следующим образом:

CREATE TABLE `random_code` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_code` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Как видно из оператора определения структуры данных таблицы базы данных, мы устанавливаем код поля номера заказа уникальным! Следовательно, если порядковые номера, сгенерированные высокой параллельной многопоточностью, будут повторяться, то при вставке в таблицу базы данных неизбежно возникнут ошибки.

Теперь давайте начнем с нашего первого метода: генерировать номера заказов на основе случайных чисел.

(1) Во-первых, создать класс Thread.Логика выполнения его метода запуска заключается в создании номера заказа и вставке сгенерированного номера заказа в таблицу базы данных.Код выглядит следующим образом:

/**
 * 随机数生成的方式-Thread
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateThread implements Runnable{
 
    private RandomCodeMapper randomCodeMapper;
 
    public CodeGenerateThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }
 
    @Override
public void run() {
    //生成订单编号并插入数据库
        RandomCode entity=new RandomCode();
        entity.setCode(RandomUtil.generateOrderCode());
        randomCodeMapper.insertSelective(entity);
    }
}

Среди них логика генерации RandomUtil.generateOrderCode() реализована с помощью ThreadLocalRandom, а его полный исходный код выглядит следующим образом:

/**
 * 随机数生成util
 * @Author:debug (SteadyJack)
 * @Date: 2019/6/20 21:05
 **/
public class RandomUtil {
    private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS");
 
    private static final ThreadLocalRandom random=ThreadLocalRandom.current();
    //生成订单编号-方式一
    public static String generateOrderCode(){
        //TODO:时间戳+N为随机数流水号
        return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
    }
 
    //N为随机数流水号
    public static String generateNumber(final int num){
        StringBuffer sb=new StringBuffer();
        for (int i=1;i<=num;i++){
            sb.append(random.nextInt(9));
        }
        return sb.toString();
    }
}

(2) После разработки метода запроса в контроллере BaseController цель состоит в том, чтобы смоделировать логику внешнего триггера с высокой степенью параллелизма для создания нескольких потоков и генерации номеров заказов.Здесь мы временно используем 1000 потоков для моделирования, и его исходный код следующим образом:

@Autowired
private RandomCodeMapper randomCodeMapper;
 
//测试在高并发下多线程生成订单编号-传统的随机数生成方法
@RequestMapping(value = "/code/generate/thread",method = RequestMethod.GET)
public BaseResponse codeThread(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

(3) После этого вы можете запустить весь проект и систему во внешнем коте, затем открыть почтальон и инициировать запрос Http Get.Ссылка на запрос: http://127.0.0.1:8092/kill/base/code/ generate/thread внимательно наблюдайте за выводом консоли, вы увидите некоторые вещи, которые вас беспокоят:

Будет "повторяющийся номер заказа, сгенерированный повторно"! Более того, откройте таблицу базы данных для наблюдения, и вы обнаружите, что «там всего более 900 записей, сгенерированных 1000 гребаных потоков, генерирующих порядковые номера». Как показано ниже:

Поэтому мы можем передать этот метод генерации уникального идентификатора или номера заказа на основе случайных чисел (конечно, когда параллелизм не очень высокий, этот метод все еще широко используется, потому что он простой и понятный какой!)

Поскольку этот метод «на основе случайных чисел» не соответствует нашим требованиям в сценариях с высокой степенью параллелизма, далее мы представим другой популярный и типичный метод, а именно «алгоритм генерации распределенного уникального идентификатора — алгоритм снежинки».

Для ознакомления с «Алгоритмом снежинки» вы можете обратиться по этой ссылке на Github, я думаю, она достаточно понятна: https://github.com/souyunku/SnowFlake, подробный Debug здесь повторяться не будет. , частичный обзор выдержка ниже:

В распределенной среде алгоритм SnowFlake может эффективно генерировать уникальные идентификаторы.Я думаю, что очень важным моментом является то, что его базовая реализация достигается за счет«Битовые операции»Достичь, говоря простым языком, это иметь дело непосредственно с машиной! Структура хранения (64-разрядная) его базовых данных показана на следующем рисунке:

Далее, давайте сгенерируем номер заказа, необходимый в системе seckill, непосредственно на основе алгоритма снежинки!

(1) Таким же образом мы сначала определяем класс Thread, а логика реализации его метода run заключается в том, чтобы сгенерировать номер заказа с помощью алгоритма снежинки и вставить его в базу данных.

/** 基于雪花算法生成全局唯一的订单编号并插入数据库表中
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateSnowThread implements Runnable{
 
    private static final SnowFlake SNOW_FLAKE=new SnowFlake(2,3);
 
    private RandomCodeMapper randomCodeMapper;
 
    public CodeGenerateSnowThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }
 
    @Override
    public void run() {
        RandomCode entity=new RandomCode();
        //采用雪花算法生成订单编号
        entity.setCode(String.valueOf(SNOW_FLAKE.nextId()));
        randomCodeMapper.insertSelective(entity);
    }
}


Среди них метод SNOW_FLAKE.nextId() — это логика использования алгоритма снежинки для генерации глобально уникального номера заказа Полный исходный код выглядит следующим образом:

/** * 雪花算法
 * @author: zhonglinsen
 * @date: 2019/5/20
 */
public class SnowFlake {
    //起始的时间戳
    private final static long START_STAMP = 1480166465631L;
 
    //每一部分占用的位数
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5;//数据中心占用的位数
 
    //每一部分的最大值
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
 
    //每一部分向左的位移
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
 
    private long dataCenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStamp = -1L;//上一次时间戳
 
    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_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
    public synchronized long nextId() {
        long currStamp = getNewStamp();
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
 
        if (currStamp == lastStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }
 
        lastStamp = currStamp;
 
        return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分
                | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }
 
    private long getNextMill() {
        long mill = getNewStamp();
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }
 
    private long getNewStamp() {
        return System.currentTimeMillis();
    }
}


(2) Затем мы разрабатываем метод запроса в BaseController, чтобы имитировать сценарий, в котором внешний интерфейс запускает высокий уровень параллелизма и генерирует многопоточный захват ордеров.

/**
 * 测试在高并发下多线程生成订单编号-雪花算法
 * @return
 */
@RequestMapping(value = "/code/generate/thread/snow",method = RequestMethod.GET)
public BaseResponse codeThreadSnowFlake(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateSnowThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}


(3) После завершения мы используем Postman, чтобы инициировать запрос Http Get, и ссылка запроса выглядит следующим образом: http://127.0.0.1:8092/kill/base/code/generate/thread/snow, наблюдайте за выводом Информация консоли, вы можете увидеть «сцену мира», а затем понаблюдать за записями таблицы базы данных, вы можете обнаружить, что 1000 потоков успешно запущены и сгенерированы 1000 соответствующих номеров заказов, как показано на следующем рисунке:



Кроме того, вы также можете настроить количество потоков от 1000 до 10000, 100000 или даже 1000000, а затем наблюдать за выводом информации консоли и записями таблицы базы данных и так далее.

Debug лично протестировал сценарии 1w и 10w, и проблем нет.Тест количества потоков 100w будет передан друзьям, чтобы попробовать (это займет много времени, так что будьте готовы!) На этом этапе мы Логика алгоритма снежинки генерации глобально уникального номера заказа применяется к нашей «логике обработки seckill», то есть его код (в методе commonRecordKillSuccessInfo KillService) выглядит следующим образом:

ItemKillSuccess entity=new ItemKillSuccess();
String orderNo=String.valueOf(snowFlake.nextId());//雪花算法
entity.setCode(orderNo); 
//其他代码省略

Пополнить:

1. В настоящее время общая конструкция и кодирование этой системы мгновенного уничтожения завершены Полный адрес базы данных исходного кода можно скачать здесь: gitee.com/steadyjack/… Помните Fork and Star! ! !