目录
背景
规则
问题
分析
思路
数据库
线程锁
方案
讨论
半年以前做的一个流程相关的项目,近期在做性能测试;之前的功能测试已经做完了,都没有什么问题。
项目采用的springmvc框架,生成订单号以及存储订单号都是在activiti的监听service中进行的。项目业务数据库和activiti数
据库是分离的。代码流程为 业务service-->监听service-->业务service。
这里重点说明一下我们系统的订单号生成规则:项目名-YYYYMMDD-(从当年1月1日计目前的订单数量+1)COUNT;即
订单号的生成由3部分组成(项目名、日期、COUNT),其中项目名固定,日期由系统生成,最后一个COUNT是需要通过读取
数据库来进行计算的。
在性能测试时发现在多线程情况下生成的订单号是重复的,导致了后台报错,只一条创建成功,其他创建都以失败告终。
之前的代码如下,没有考虑到多线程高并发情况下的线程案例问题。
private String getNum(){
AtomicInteger count = new AtomicInteger(数据库获取大于今年的个数);
return String.format("%04d", count.incrementAndGet());
}
面对多线程的情况,之前想到了2个思路:数据库和线程锁
从数据库层面解决,直接在数据库创建一个function用来生成订单号,在插入数据的时候就调用这个function以此来解决高并发的情
况,这种方式如果能实现效果是最好的。
create or replace function fillWords() returns varchar as $fillWords$
declare
temp_word varchar;
temp_count int4;
begin
SELECT
(COUNT (T .node_id) + 1) into temp_count
FROM
t_base_order T
WHERE
T .start_time >= date_trunc('year', now());
temp_word := temp_count || '';
if(length(temp_word) < 4) then
temp_word := lpad(temp_word, 4, '0');
end if;
return 'RMS-' || to_char(now(), 'yyyyMMdd') || temp_word;
end;
$fillWords$ language plpgsql;
使用这种方式试了一下,还是会生成重复的原因。后来一分析发发现,单条插入数据时的事务并没有commit,因此在高并发情况下
查询的COUNT还是一样的(数据库默认的隔离级别为Read committed);一想那我就修改隔离等级吧,一查,我勒个去,postgresql直接
不支持Read uncommitted,那个伤心......
那就来做提交事务吧,项目采用的是声明式事务,如果要单个提交事务就需要编程式事务,修改了一下午,是各种报错,也可能是
我自己没有修改对吧。总之这种手动事务的方式最终是没有成功。
听性能测试的同事说,线程锁这种形式非常损耗性能,但没办法了,数据库的方案形不通,那就只能这种方式了呗。
1、最开始的想法是使用LOCK直接把生成订单号和插入数据的代码块直接锁起来,但不知道为什么还是有重复的出现;后来一想,
也是事务没有提交导致的。
2、听另外一个同事说,加到代码块上不行,应该把锁加到方法上,于是一试,还是重复,崩溃呀。
想了2天也不知道使用什么方法,网上百度的都是使用随机数来进行处理,但与我们项目的规则不符合呀。
最终突然想到一个办法,就是利用JAVA的AtomicInteger来实现,因此这个类是线程安全的。思路如下:
第一次的时候,直接把查询的个数加载到static变量中,然后利用这个变量的自增来给COUNT设值,感觉这个方案不错,一试还真
行,效率也不差。具体实现,请看最后一节。
直接贴代码:
private static AtomicInteger count = new AtomicInteger(0);
private String getNum(){
if(count.get() == 0){
LOG.error("+++++++++"+count.get());
synchronized(count){
LOG.error("----------"+count.get());
//下面这个再次判断很重要,防止了多次设值
if(count.get() == 0){
count.set(数据库获取大于今年的个数);
LOG.error("**********"+count.get());
}
}
}
return String.format("%04d", count.incrementAndGet());
}
这种实现方案,只是第一次并发的情况下加锁损耗了性能,第二次并发的情况下就不会有锁的影响了;并且由于锁之中的再次判
断,既防止了多次设值,也提交了效率,这个设计不错(自我感觉哈^_^)。
最后的这个方案其实也有问题,就是如果项目要做负载均衡的情况下,也会出现重复单号的情况,因此如果项目要做分布式部署的
情况下,可以将这段代码发布成一个公用服务,所有的分布式部署都调用这个公用服务,这样就不会有问题了。但我们项目不会做分布式
部署,因此就不考虑公用服务这种方案了。
各位有什么好的想法,也希望大家一起交流哈。