订单号生成工具类

引言

    许多企业系统都涉及到了订单号的生成。订单号可以帮我们我们标识用户的一次行为。因此它必须是全局唯一的。我们当然可以采用类似UUID这种全时空唯一的字符串来标识一个订单,但是UUID对于用户和我们自己来说都过于复杂了,用户无法记忆甚至无法用它来要求客服查询。一个好的订单号在保证简单、唯一性的情况下,应该具有自解性。根据这个订单号我们可以解读出用户购买的业务、购买的时间等信息。下面介绍几种订单号的生成方案。

解决方案

加锁和时间戳

    如果是在小型单台服务的系统上,订单号的生成可以采用简单的时间戳(精确到毫秒)配合JVM级别的锁来实现。但是更常见的情况是,我们出于系统高可用、负载均衡等方面的考虑,部署的架构往往是多服务器的。这种情况下,就必须重新考虑订单号的生成策略了。

数据库自增ID

利用数据库的自增ID,结合当前时间戳或者业务编号,是一种非常常见的订单号生成方式。有两种实现方式:

  1. 新建一张表定义自增列,在应用层获取该列的自增值

  2. 新建一张序列表,保存自增序列的当前值

这两种实现方式各有优劣:

方式1必须定义该列的字段足够大,否则可能达到最大值后会报错。

方式2序列的增长在应用层,必须保证事物。

它们共同的问题是:

在生成订单号时,我们都需要进行一次数据库操作,在高并发高访问的情况下,容易造成数据库压力过大,形成性能瓶颈。

系统足够庞大,进行分库分表之后。又会带来保证序列唯一性的额外问题。

它们很容易泄漏商品的销量和操作次数这些敏感信息

集中式ID管理

 常见的方式有:

  1. 将自增序列保存到集中式or分布式缓存中,由于集群服务是共享分布式缓存的,因此很好地解决了订单号的唯一性问题,同时缓存具有足够的伸缩性,也可以做高可用、持久化。

  2. 订单号服务,独立的订单号生成管理服务,提供给各业务线进行调用。

集中式ID管理的优势很明显,无论是性能扩展还是高可用方面都有非常好的解决方案。劣势就是系统较为庞大,需要搭建专门的缓存服务(如redis、tair、memcached等),订单号服务为了避免单点故障还需要做冗余。

系统标识的订单号生成方案

我们可以根据系统的一些唯一属性来生成唯一的订单号,这个属性可以是服务器的IP、机器码、MAC地址等,结合JVM锁,既避免了数据库自增ID的性能瓶颈问题,又无需集中式ID管理的大材小用。下面是我的一个简单实现方案。

是通过时间戳+IP后两位+自增序列+随机数来生成的订单号,使用可重入锁代替内置锁(synchronized)以解决在高并发请求下的细微性能问题。

package com.yaphis.commons.util.order;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 订单号生成器
 * 
 * @author Yaphis 2015年4月29日 下午7:12:44
 */
public class OrderGenerater {

    private static final Logger LOG = LoggerFactory.getLogger(OrderGenerater.class);

    private volatile static int serialNo = 0;

    private static final String FORMATSTRING = "yyMMddHHmmssSSS";

    /**
     * 使用公平锁防止饥饿
     */
    private static final Lock lock = new ReentrantLock(true);

    private static final int TIMEOUTSECODES = 3;

    /**
     * 生成订单号,生成规则 时间戳+机器IP最后两位+2位随机数+两位自增序列 <br>
     * 采用可重入锁减小锁持有的粒度,提高系统在高并发情况下的性能
     * 
     * @param buinessId
     * @return
     */
    public static String generateOrder() {
        StringBuilder builder = new StringBuilder();
        builder.append(getDateTime(FORMATSTRING)).append(getLastNumOfIP());
        builder.append(getRandomNum()).append(getIncrement());
        return builder.toString();
    }

    /**
     * 获取系统当前时间
     * 
     * @param formatStr
     * @return
     */
    private static String getDateTime(String formatStr) {
        SimpleDateFormat format = new SimpleDateFormat(formatStr);
        return format.format(new Date());
    }

    /**
     * 获取自增序列
     * 
     * @return
     */
    private static String getIncrement() {
        int tempSerialNo = 0;
        try {
            if (lock.tryLock(TIMEOUTSECODES, TimeUnit.SECONDS)) {
                if (serialNo >= 99) {
                    serialNo = 0;
                } else {
                    serialNo = serialNo + 1;
                }
                tempSerialNo = serialNo;
            } else {
                // 指定时间内没有获取到锁,存在激烈的锁竞争或者性能问题,直接报错
                LOG.error("can not get lock in:{} seconds!", TIMEOUTSECODES);
                throw new RuntimeException("generateOrder can not get lock!");
            }
        } catch (Exception e) {
            LOG.error("tryLock throws Exception:", e);
            throw new RuntimeException("tryLock throws Exception!");
        } finally {
            lock.unlock();
        }
        if (tempSerialNo < 10) {
            return "0" + tempSerialNo;
        } else {
            return "" + tempSerialNo;
        }
    }

    /**
     * 返回两位随机整数
     * 
     * @return
     */
    private static String getRandomNum() {
        int num = new Random(System.nanoTime()).nextInt(100);
        if (num < 10) {
            return "0" + num;
        } else {
            return num + "";
        }
    }

    /**
     * 获取IP的最后两位数字
     * 
     * @return
     */
    private static String getLastNumOfIP() {
        String ip = getCurrentIP();
        return ip.substring(ip.length() - 2);
    }

    /**
     * 获取本机IP
     * 
     * @return
     */
    private static String getCurrentIP() {
        String ip = "";
        try {
            ip = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            LOG.error("getLocalHost throws UnknownHostException:", e);
            throw new RuntimeException("can not get ip!");
        }
        if (StringUtils.isBlank(ip)) {
            LOG.error("ip is blank!");
            throw new RuntimeException("ip is blank!");
        }
        return ip;
    }
}

附一段测试代码

package com.yaphis.commons.util.order;

import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 订单号生成器单元测试
 * 
 * @author Yaphis 2015年5月1日 下午3:17:51
 */
public class OrderGeneraterTest {

    private static final Logger LOG = LoggerFactory.getLogger(OrderGeneraterTest.class);

    /**
     * 验证订单号生成是否会重复和耗时(单线程)
     */
    @Test
    public void testGenerateOrder() {
        List<String> orderList = new ArrayList<String>();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            String orderId = OrderGenerater.generateOrder();
            if (orderList.contains(orderId)) {
                LOG.info("orderId:{}", orderId);
                LOG.info("orderList:{},list:{}", new Object[] { orderList.size(), orderList });
                Assert.fail("订单号重复!");
            }
            orderList.add(orderId);
        }
        LOG.info("generateOrder cost:{}", System.currentTimeMillis() - startTime);
    }

    /**
     * 验证订单号生成是否会重复和耗时(多线程)
     */
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        long startTime = System.currentTimeMillis();
        // 模拟1000个并发请求
        for (int j = 0; j < 10000; j++) {
            new Thread(new ThreadTest(list)).start();
        }
        LOG.info("testGenerateOrderMultithread cost:{}", System.currentTimeMillis() - startTime);
    }

    /**
     * 测试线程
     * 
     * @author Yaphis 2015年5月1日 下午3:59:19
     */
    public static class ThreadTest implements Runnable {

        private List<String> list;

        public ThreadTest(List<String> list) {
            this.list = list;
        }

        public void run() {
            for (int i = 0; i < 1; i++) {
                String orderId = OrderGenerater.generateOrder();
                if (list.contains(orderId)) {
                    LOG.error("订单号重复!");
                    break;
                }
                list.add(orderId);
            }
        }
    }
}

其他:

以上实现其实还比较简陋,许多情况没有考虑。例如多网卡情况下的多IP问题等、欢迎大家交流指正!

转载于:https://my.oschina.net/yaphis/blog/408933

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值