首页 > 技术文章 > 分布式环境利用数据库生成连续唯一序列

doflamingo 2020-08-21 21:40 原文

生成连续唯一的序列号在很多业务场景都会需要,本文分享一个在分布式环境利用数据库生成连续唯一序列的例子(按天生成),在并发不是特别高的大型场景还是值得一干。文中采用版本(version)机制,结合自旋锁 + 乐观锁生成连续的唯一的数字。

直接上代码

1. 创建表test_no

CREATE TABLE `test_no` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `date` varchar(10) NOT NULL COMMENT '格式yyyyMMdd',
  `number` bigint(20) unsigned DEFAULT NULL COMMENT '序列号',
  `version` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '版本',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_date` (`date`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;

date字段建有唯一索引。

2. MyBaties持久层代码TestNoMapper

package com.mingo.exp.generate_number.single_db_cas;

import com.mingo.exp.generate_number.single_db_cas.dto.TestNoDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * @author Doflamingo
 */
public interface TestNoMapper {

    /**
     * 插入一条数据。主要用于初始值插入
     *
     * @param noDO
     */
    @Insert("INSERT INTO test_no(`date`,number,version) VALUES(#{date}, #{number}, #{version})")
    void insert(TestNoDO noDO);

    /**
     * 查询数据。date 具有唯一键
     *
     * @param date
     * @return
     */
    @Select("SELECT date,number,version FROM test_no WHERE `date` = #{date}")
    TestNoDO select(@Param("date") String date);

    /**
     * 只有与当前版本号一样才能更新
     *
     * @param date
     * @param version
     * @return
     */
    @Update("UPDATE test_no SET number = number + 1, version = version + 1 WHERE `date` = #{date} AND version = #{version}")
    int update(@Param("date") String date, @Param("version") Integer version);
}

3. 生成连续唯一序列代码DistributeSerialNumber

package com.mingo.exp.generate_number.single_db_cas;

import com.mingo.exp.generate_number.single_db_cas.dto.TestNoDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 分布式环境利用数据库生成连续唯一序列
 *
 * @author Doflamingo
 */
@Component
public class DistributeSerialNumber {

    @Autowired
    private TestNoMapper testNoMapper;

    /**
     * 查询唯一号码
     *
     * @param date 格式 yyyyMMdd
     * @return
     */
    public int get(String date) {
        TestNoDO testNoDO = this.selectOrInsert(date);
        boolean flag = this.update(testNoDO);
        if (flag) {
            return testNoDO.getNumber();
        }
        // 自旋锁 + 乐观锁
        while (!flag) {
            testNoDO = testNoMapper.select(date);
            // 更新number
            flag = this.update(testNoDO);
        }
        return testNoDO.getNumber();
    }

    /**
     * 这里主要用于当前首次查询和插入数据,保证直插入一条数据,date建有唯一索引;
     * 并发度不大也可以不用双重校验锁,直接插入,异常再查询即可
     *
     * @Param date yyyyMMdd
     */
    private TestNoDO selectOrInsert(String date) {
        TestNoDO testNoDO = testNoMapper.select(date);
        if (null == testNoDO) {
            try {
                testNoDO = new TestNoDO(date, 1, 0);
                synchronized (this) {
                    TestNoDO testNoDO2 = testNoMapper.select(date);
                    if (null != testNoDO2) {
                        return testNoDO2;
                    }
                    testNoMapper.insert(testNoDO);
                }
            } catch (Exception e) {
                // 插入失败,其他机器已经插入了
                testNoDO = testNoMapper.select(date);
            }
        }
        return testNoDO;
    }

    /**
     * 按版本号更新数据
     *
     * @param testNoDO
     * @return true 即 成功
     */
    private boolean update(TestNoDO testNoDO) {
        return 1 == testNoMapper.update(testNoDO.getDate(), testNoDO.getVersion());
    }
}

4. 测试类DistributeSerialNumberTest

为了测试效果,我用20线程生成序列号

package com.mingo.exp.generate_number.single_db_cas;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@RunWith(SpringRunner.class)
public class DistributeSerialNumberTest {

    @Autowired
    private DistributeSerialNumber serialNumber;

    @Test
    public void test() throws Exception {
        System.out.println("\n\n==============================\n");

        // 20个线程
        ExecutorService executorService =
                new ThreadPoolExecutor(
                        20,
                        20,
                        0,
                        TimeUnit.MILLISECONDS,
                        new LinkedBlockingDeque<>()
                );

        for (int i = 30; i > 0; i--) {
            // 启动
            executorService.execute(() -> {
                int number = serialNumber.get("20200608");
                System.out.println("获得的数:" + number);
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
    }
}

 

5. 测试结果

获得的数:2
获得的数:1
获得的数:3
获得的数:4
获得的数:6
获得的数:5
获得的数:13
获得的数:8
获得的数:10
获得的数:14
获得的数:9
获得的数:11
获得的数:7
获得的数:12
获得的数:15
获得的数:18
获得的数:17
获得的数:16
获得的数:20
获得的数:26
获得的数:24
获得的数:21
获得的数:25
获得的数:27
获得的数:28
获得的数:23
获得的数:19
获得的数:22
获得的数:30
获得的数:29

推荐阅读