多服务保证订单号唯一

时间:2025-05-13 20:34:26

    以生成订单号为例:多个用户下单时,如果我们只部署了一个服务,那么在订单生成的方法上使用 synchronized 可以保证订单号唯一,但是应用部署在多个服务器上时,用户访问不同服务器上的服务时,synchronized 就不能同步了。换句话说,synchronized 只能保证一个应用中的同步。

多服务下保证订单号唯一

    环境: Spring 环境,Junit 4 测试,Spring JdbcTemplate

    原理:利用单号生成器表,记录当前的订单号,通过事务操作单号生成器表,先 update 订单号 + 1,再获取这个号码,拼接成订单号。

    表结构:

# 订单表
CREATE TABLE `tb_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_number` varchar(255) NOT NULL COMMENT '订单号',
  `creator` varchar(255) DEFAULT NULL COMMENT '服务名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

# 单号生成器
CREATE TABLE `tb_no_generator` (
  `id` varchar(255) NOT NULL COMMENT '类型',
  `next_num` int(11) NOT NULL COMMENT '计数器',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# 计数器初始化
INSERT INTO `tb_no_generator` VALUES ('order', '0');

    测试代码:

  • 线程一
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

public class ByGenerator {
    // 暂停 500 个线程
    private CountDownLatch begin = new CountDownLatch(1);
    // 启动 500 个线程
    private CountDownLatch end = new CountDownLatch(500);
    private ExecutorService exec = (500);
    private ApplicationContext ctx;
    private JdbcTemplate jt;
    private PlatformTransactionManager transactionManager;
    {
        ctx = new ClassPathXmlApplicationContext("classpath:config/spring/**");
        jt = (JdbcTemplate) ("jdbcTemplate");
        transactionManager = (PlatformTransactionManager) ("transactionManager");
    }

    @Test
    public void test() throws InterruptedException {
        // 睡眠 2s,因为要启动两个单元测试,即每个单元测试都加载一次配置文件,相当于两个服务
        // 为了保证启动第二个单元测试时,减小手动启动的时间误差,因此睡眠 2s
        (2000L);
        for (int i = 0; i < 500; i++) {
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 使创建好的线程全部等待
                        ();
                        // 开启事务,如果没有开启事务,那么订单号会重复。在实际开发中,在 service 层加上 @Transactional 注解即可,这里因为是单元测试,因此我使用了编程式事务
                        TransactionDefinition def = new DefaultTransactionDefinition();
                        TransactionStatus status = (def);
                        ("insert into tb_order (order_number,creator) values ( " + generate()
                                + ", 'ByGenerator')");
                        // 提交事务
                        (status);
                    } catch (InterruptedException e) {
                        ();
                    } finally {
                        // 每创建一个线程,计数器 -1
                        ();
                    }
                }
            };
            (run);
        }
        // 当执行完 for 循环时,释放所有线程
        ();
        ();
        ();
    }

    private String generate() {
        // 获取今天的日期
        String date = (new Date(), "yyyyMMdd");
        // 首先将单号生成器表 tb_no_generator 的计数器 +1
        ("update tb_no_generator set next_num= next_num + 1 where id = 'order' ");
        // 查询单号
        String next_num = ("select next_num from tb_no_generator where id='order' ", );
        // 返回数据类似于 20180403001、20180403100
        return date
                + (next_num.length() == 3 ? next_num : (next_num.length() == 2 ? ("0" + next_num) : ("00" + next_num)));
    }

}

class DateUtils {
    private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();
    private static final Object lockObj = new Object();
    private static SimpleDateFormat getSdf(final String pattern) {
        ThreadLocal<SimpleDateFormat> simpleDateFormat = (pattern);
        // 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf
        if (simpleDateFormat == null) {
            synchronized (lockObj) {
                simpleDateFormat = (pattern);
                if (simpleDateFormat == null) {
                    simpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
                        @Override
                        protected SimpleDateFormat initialValue() {
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    (pattern, simpleDateFormat);
                }
            }
        }
        return ();
    }
    public static String format(Date date, String pattern) {
        return getSdf(pattern).format(date);
    }
}
  • 线程二
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

public class ByGenerator2 {
    private CountDownLatch begin = new CountDownLatch(1);
    private CountDownLatch end = new CountDownLatch(500);
    private ExecutorService exec = Executors.newFixedThreadPool(500);
    private ApplicationContext ctx;
    private JdbcTemplate jt;
    private PlatformTransactionManager transactionManager;
    {
        ctx = new ClassPathXmlApplicationContext("classpath:config/spring/**");
        jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        transactionManager = (PlatformTransactionManager) ctx.getBean("transactionManager");
    }
    @Test
    public void test() throws InterruptedException {
        for (int i = 0; i < 500; i++) {
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        begin.await();
                        TransactionDefinition def = new DefaultTransactionDefinition();
                        TransactionStatus status = transactionManager.getTransaction(def);
                        jt.update("insert into tb_order (order_number,creator) values ( " + generate()
                                + ", 'ByGenerator2' )");
                        transactionManager.commit(status);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        end.countDown();
                    }
                }
            };
            exec.execute(run);
        }
        begin.countDown();
        end.await();
        exec.shutdown();
    }
    private String generate() {
        String date = DateUtils.format(new Date(), "yyyyMMdd");
        jt.execute("update tb_no_generator set next_num= next_num + 1 where id = 'order' ");
        String next_num = jt.queryForObject("select next_num from tb_no_generator where id='order' ", String.class);
        return date
                + (next_num.length() == 3 ? next_num : (next_num.length() == 2 ? ("0" + next_num) : ("00" + next_num)));
    }
}

验证生成数据是否有重复:

SELECT * FROM tb_order a JOIN tb_order b ON a.order_number = b.order_number WHERE  = 'ByGenerator' AND  = 'ByGenerator' and !=

SELECT * FROM tb_order a JOIN tb_order b ON a.order_number = b.order_number WHERE  = 'ByGenerator2' AND  = 'ByGenerator2' and !=

SELECT * FROM tb_order a JOIN tb_order b ON a.order_number = b.order_number WHERE  = 'ByGenerator' AND  = 'ByGenerator2' and !=

SELECT * FROM tb_order a JOIN tb_order b ON a.order_number = b.order_number WHERE  = 'ByGenerator2' AND  = 'ByGenerator' and !=

    注:本人对并发与分布式事务认知尚浅,本文如有问题,望您指教。