基于数据库的分布式id生成器

现在分布式越来越普遍了,怎么在分布式服务设计一套id生成策略呢?这里提供一套基于数据配置的分段自增id生成工具。基本满足同数据库多服务共用的id生成需求。另外可根据需要灵活配置,操作方便。

1.大体思路

首先需求保证单应用内的线程安全,使用传统的synchronized来解决。采用数据库的隔离级别和对比老值得方式,实现多服务之间的数据安全,避免出现id重复现象。此外,每次取一段值保存在服务中,能够减少对数据库的访问,提升性能。

2.数据库准备
CREATE TABLE `table_id` (
  `key` varchar(50) NOT NULL,
  `value` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

key为表的名称,value为表的id值。服务每次从数据库取值时都会修改value为新的值。

3.代码

id工具类

package com.david.concurrentdemo2;

import com.david.concurrentdemo2.service.UserServiceImpl;
import com.david.concurrentdemo2.util.SpringContextHolder;

import java.util.HashMap;
import java.util.Map;

/**
 * @author David
 * @descritpion id生成工具
 *  思路: 首先在单机内线程安全,然后利用mysql的隔离级别,保证每次只能有一个机器获取id成功,每次获取2000个id缓存起来,
 *  使用完再次获取
 * @date 2019/10/22
 */
public class IdUtil {
    /**
     * 定义一个变量作为id基数
     */
    private static long idNum = 0;
    /**
     * 每次的id缓存
     */
    private static int idcache = 0;
    /**
     * 每次的增长数
     */
    private final static int stepNum = 2000;

    private static UserServiceImpl userService ;

    /**
     * 首次调用从数据库获取当前id的值
     */
    static {
        userService = SpringContextHolder.getBean(UserServiceImpl.class);

        idNum =  userService.getTableIdByKey("test");
    }


    public synchronized static long getId() {
        //每次获取的id的值为基础值加上缓存的数值,如果缓存中用完了,则刷新id基础值和缓存
        if (idcache > 0) {
            return idNum + (stepNum - --idcache);
        } else {
            if (refreshId()) {
                return idNum + (stepNum - --idcache);
            }
            return -1;
        }
    }

    /**
     *  每次刷新的时候,拿老的id和数据对比,一样则修改成功。不一样修改失败。
     *  修改失败后,从数据库获取当前的id作为基础id,再次进行修改。此过程共可执行三次。
     *  修改成功则从新设置基础的id和缓存id
     * @return
     */
    private static boolean refreshId() {

        for (int i = 0; i < 3; i++) {
            try {
                if (updateTableId( idNum + stepNum) ){
                    idNum = idNum + (i+1) * stepNum;
                    idcache = stepNum;
                    return true;
                } else {
                    //重新从数据库获取id位置
                    idNum =  userService.getTableIdByKey("test");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 修改成功返回true,修改失败返回false。修改过程中出现异常,会进行再次修改。最多执行三次。
     * 利用数据库的隔离级别和对比旧值来实现不同服务间的数据安全。
     * @param num
     * @return
     */
    public static boolean updateTableId(long num) {
        Map map = new HashMap<>();
        map.put("key", "test");
        map.put("oldValue", num - stepNum);
        map.put("value", num);

        for (int i = 0; i < 3; i++) {
            try{
                if(userService.updateTableId(map) == 1) {
                    return true;
                }
                return false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

测试代码

public void test1() {
        List<Long> longL = Collections.synchronizedList(new ArrayList<>());
        Set<Long> longS = Collections.synchronizedSet(new HashSet<>());

        CountDownLatch count = new CountDownLatch(4);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 4; i++) {
            new Thread(() ->{
                for (int j = 0; j < 20000; j++) {
                    long id = IdUtil.getId();
                    longL.add(id);
                    longS.add(id);
                    //System.out.println(id);
                }
                count.countDown();
            }).start();
        }

        try {
            count.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("user time:" + (end - start));

        System.out.println(longL.size());
        System.out.println(longS.size());

        longL.forEach(ele -> {
            userService.insertResult(ele);
        });
    }

多线程下id生成没有问题,最后把id插入验证表(主键唯一),插入无异常。

	/**
     * 耗时1.626
     */
    @Test
    public void test2() {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 1000000; i++) {
            IdUtil.getId();
        }

        long end = System.currentTimeMillis();
        System.out.println("use time:" + (end - start));
    }

两个应用单台机器上同时跑,单个应用100万个id的获取时间在1.5s左右。

4.总结

可根据实际的需求设置缓存数据的大小。使用数据库记录id值速度上比不上redis,安全性会好一些。请大神指教!

你可能感兴趣的:(多线程,id)