多服务保证订单号唯一

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_30038111/article/details/79800401

    以生成订单号为例:多个用户下单时,如果我们只部署了一个服务,那么在订单生成的方法上使用 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 java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
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 ByGenerator {
    // 暂停 500 个线程
    private CountDownLatch begin = new CountDownLatch(1);
    // 启动 500 个线程
    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 {
        // 睡眠 2s,因为要启动两个单元测试,即每个单元测试都加载一次配置文件,相当于两个服务
        // 为了保证启动第二个单元测试时,减小手动启动的时间误差,因此睡眠 2s
        Thread.sleep(2000L);
        for (int i = 0; i < 500; i++) {
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 使创建好的线程全部等待
                        begin.await();
                        // 开启事务,如果没有开启事务,那么订单号会重复。在实际开发中,在 service 层加上 @Transactional 注解即可,这里因为是单元测试,因此我使用了编程式事务
                        TransactionDefinition def = new DefaultTransactionDefinition();
                        TransactionStatus status = transactionManager.getTransaction(def);
                        jt.update("insert into tb_order (order_number,creator) values ( " + generate()
                                + ", 'ByGenerator')");
                        // 提交事务
                        transactionManager.commit(status);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 每创建一个线程,计数器 -1
                        end.countDown();
                    }
                }
            };
            exec.execute(run);
        }
        // 当执行完 for 循环时,释放所有线程
        begin.countDown();
        end.await();
        exec.shutdown();
    }

    private String generate() {
        // 获取今天的日期
        String date = DateUtils.format(new Date(), "yyyyMMdd");
        // 首先将单号生成器表 tb_no_generator 的计数器 +1
        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);
        // 返回数据类似于 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 = sdfMap.get(pattern);
        // 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf
        if (simpleDateFormat == null) {
            synchronized (lockObj) {
                simpleDateFormat = sdfMap.get(pattern);
                if (simpleDateFormat == null) {
                    simpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
                        @Override
                        protected SimpleDateFormat initialValue() {
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    sdfMap.put(pattern, simpleDateFormat);
                }
            }
        }
        return simpleDateFormat.get();
    }
    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 a.creator = 'ByGenerator' AND b.creator = 'ByGenerator' and a.id!=b.id

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

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

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

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

展开阅读全文

高并发,保证订单号唯一

07-17

这是我在网上看来的。求大家验证rnrnCREATE TABLE [dbo].[SerialNo]( rn [sCode] [varchar](50) NOT NULL,--主键也是多个流水号的类别区分 rn [sName] [varchar](100) NULL,--名称,备注形式 rn [sQZ] [varchar](50) NULL,--前缀 rn [sValue] [varchar](80) NULL,--因子字段 rn CONSTRAINT [PK_SerialNo] PRIMARY KEY CLUSTERED rn( rn [sCode] ASC rn)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, rn rn ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] rn) ON [PRIMARY] rnrnrn==========================================================================rnrnCreate procedure [dbo].[GetSerialNo] rn( rn @sCode varchar(50) rn) rn rn as rn rn--exec GetSerialNo rn rnbegin rn rn Declare @sValue varchar(16), rn rn @dToday datetime, rn rn @sQZ varchar(50) --这个代表前缀 rn rn Begin Tran rn rn Begin Try rn rn -- 锁定该条记录,好多人用lock去锁,起始这里只要执行一句update就可以了 rn --在同一个事物中,执行了update语句之后就会启动锁 rn Update SerialNo set sValue=sValue where sCode=@sCode rn rn Select @sValue = sValue From SerialNo where sCode=@sCode rn rn Select @sQZ = sQZ From SerialNo where sCode=@sCode rn rn -- 因子表中没有记录,插入初始值 rn rn If @sValue is null rn rn Begin rn rn Select @sValue = convert(bigint, convert(varchar(6), getdate(), 12) + '000001') rn rn Update SerialNo set sValue=@sValue where sCode=@sCode rn rn end else rn rn Begin --因子表中没有记录 rn rn Select @dToday = substring(@sValue,1,6) rn rn --如果日期相等,则加1 rn rn If @dToday = convert(varchar(6), getdate(), 12) rn rn Select @sValue = convert(varchar(16), (convert(bigint, @sValue) + 1)) rn rn else --如果日期不相等,则先赋值日期,流水号从1开始 rn rn Select @sValue = convert(bigint, convert(varchar(6), getdate(), 12) +'000001') rn rn rn rn Update SerialNo set sValue =@sValue where sCode=@sCode rn rn End rn rn Select result = @sQZ+@sValue rn rn Commit Tran rn rn End Try rn rn Begin Catch rn rn Rollback Tran rn rn Select result = 'Error' rn rn End Catch rn rnend 论坛

没有更多推荐了,返回首页