并发申请业务号

14 篇文章 0 订阅
2 篇文章 0 订阅

并发申请业务号

起因

有个表,打个比方,申请表,就是说,客户申请的时候,填写记录到申请记录表,表里新增了一条记录,当初设计的时候,表里没有业务号这个字段,只有id,然后一堆信息,然后新需求来了,导出这个记录表到excel里,然后把excel这个文件传到其他系统里,后台人员定时的去取这个文件填写。然后这个文件又回写到这个系统里。那么问题来了,导出到excel的时候,excel的每条记录是没有id的,也不允许导出id,这个时候,你如何确定这个excel的这条记录就是你数据库的这条的呢?

讨论后,决定用新增个字段,叫业务号。是不是熟悉起来了,用业务号标识这个记录。新增记录就是申请一个新的业务号。

业务号有这个几个要求。要求是这种格式的,QT2_202012_00000012 ,QT2标识这个业务是什么类型的,202012标识这个业务的日期月,后续的是一个7位数有序递增号,不允许重复(毕竟这个业务号标识了数据库的一条记录,重复了那还了得),然后允许跳数,就是说1以后可以是3.并不一定是2。

https://juejin.cn/post/6903124890589757453

处理思路

不管什么,首先是表里新增字段,业务号这个字段。然后表里的数据肯定是没有业务号的,当然要我们要么用sql语句更新要么用程序更新。

我这里是选择了sql更新。

ALTER TABLE ito_delivery_apply
ADD COLUMN apply_no varchar(100) NULL COMMENT ‘货期查询业务号’ AFTER id; //新增业务号字段

update ito_delivery_apply INNER JOIN
(SELECT @rowno:=@rowno+1 as rowno,r.* from ito_delivery_apply r ,(select @rowno:=0) t ORDER BY r.create_date)A
on ito_delivery_apply.id =A.id
set ito_delivery_apply.apply_no = concat(“OP2_”,SUBSTRING(A.create_date, 1,4),SUBSTRING(A.create_date, 6,2),"_",SUBSTRING(A.rowno+10000000,2))

//更新业务号,思路也很简单,选择处理表新增个rowno,代表是第几个,然后,连自己,更新记录,拼接字段成一个业务号进行更新。

//注意排序,随时间递增的

生成业务号

然后重头戏来了,怎么生成业务号,最重要的是不能重复。当时想的也是很简单,

方案1.业务号加个unique约束,然后申请业务号的时候,查询最近的一次业务号,然后递增1然后插入。这个问题也贼大,你每一个插入操作,去数据库查询1次,数据量大了怎么办?行吧,也行,但是并发量也上不去的。

方案2.新建个序列表,(百度mysql的序列表),思路也很简单,弄个表,然后每次插入的时候,去查一下这个表的下个值,然后插入。但是问题来了,序列号何时清空。这个是每个月清空一次。那想想看,现在到了月初,该清序列表了,你怎么保证自己是第一个清序列的请求。

打个比方,

你来要序列号,你发现,今天是月初诶,我要去清下序列表,清好了,我美滋滋的去申请了个1,后来的兄弟不用谢,我帮你清好了。

第二个请求来要序列号,你发现,今天是月初诶,我要去清下序列表,清好了,我美滋滋的去申请了个1,后来的兄弟不用谢,我帮你清好了。

你会发现,月初的每个序列都是1,我去你大爷的。

那加个判断,今天是月初诶,我去申请下序列号,发现不是1诶,说明没人清过,那我去清。

然后这个时候又来请求了,发现今天是月初诶,我去申请下序列号,发现不是1,是2诶,说明没人清过,那我去清。

还是不行。

其实这种往往是配合定时任务,月初0点去清。

也挺蠢的。

换个方案。

方案3.那我用redis来拿序列号不行吗。redis不是有个autoincr吗。我用这个,思路是,用qt2_202012做个key去找redis要值,顺便+1.这样下个请求拿到的就不是我的值了,我还考虑到get and set不是原子操作(百度原子操作),这样我用redis的RedisAtomicLong ,我厉害吧,这样就能拿到正确的序列号了吧。

思路是没错的,但是写法要注意哦,我随便百度了一篇。https://blog.csdn.net/zuokaopuqingnian/article/details/85207280

//自增方法 
    public Long incr(String key, long liveTime) {
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        Long increment = entityIdCounter.getAndIncrement();
        //初始设置过期时间
        if ((null == increment || increment.longValue() == 0) && liveTime > 0) {
            //liveTime为秒数
            entityIdCounter.expire(liveTime, TimeUnit.SECONDS);
        }
        return increment;
    }
    //生成批次号
    private String getOrder() {
        String order = "";
        //incr初始值为0,一般需求初始值为1,所以进行判断0的操作
        Long incr = this.incr("key" , 5);
        if (incr == 0) {
            //设置自增key  key 5秒后过期, 过期时间的设定只在新增key时有效
            incr = this.incr("key" , 5);
        }
        DecimalFormat df = new DecimalFormat("00000");
        order = df.format(incr);
        return order;
    }

乍一看,没啥问题的样子,但是问题还是很多的,第一,RedisAtomicLong与设置过期时间不是原子操作,容易把key设为永不过期的。(没大问题)第二。

这个生成批次号的时候,当获取到incr为0的时候,这里问题很大哦,这里如果并发高的话,想想看,

第一个请求来申请的时候,发现值为0,然后不对啊,我要1啊,那我自增1.

第二个请求来申请的时候,发现值为1,对哦,我可以要1,那我完事了,你自增为2吧。

然后发现第二个请求比第一个快,他把值设置为2了,然后第一个请求去自增0为1的时候,其实是自增2为3,于是第一个请求就是3了。

可喜可贺,1丢失了。

用redis的原子自增,问题没问题的,但是写法要注意点的。就是当置为0的时候,改怎么写的问题。

还有就是redis的原子自增,有个问题是,步长(就是每次加多少设置不了)。跟set配合的时候进行初始化的时候,注意初始化的并发问题。

方案4.用lua脚本实现redis的cas。

这个思路跟3一样,不过他的赋值是lua来做的。先看demo吧。

package com.xy.ito.product.utils;

import com.google.common.collect.ImmutableList;
import com.xy.ito.product.config.MyRedisConfig;
import com.xy.microservice.common.utils.StringUtil;
import com.xy.microservice.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @description: 构建的执行redis的rua脚本的工具类
 * @author: wzy
 * @create: 2020-12-25 17:23
 **/
@Component
@Slf4j
public class RedisLuaUtil {
    @Autowired
    private RedisTemplate<String, Serializable> limitRedisTemplate;
    @Autowired
    private MyRedisConfig myRedisConfig;

    //包装key
    public String wrapKey(MyRedisConfig keyPrefix, String key){
        if(keyPrefix==null || StringUtil.isEmptyString(keyPrefix.getCurr())){
            return key;
        }
        return new StringBuilder(keyPrefix.getModule()).append(":").append(keyPrefix.getCurr()).append(":").append(key).toString();
    }



    private String buildTestScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nlocal old ");
        lua.append("\nlocal new ");

        lua.append("\nc = redis.call('EXISTS',KEYS[1])");
        lua.append("\nif c and c==0 and tonumber(ARGV[1]) == nil then");
        lua.append("\n redis.call('set',KEYS[1],tonumber(ARGV[2]))");
        lua.append("\nreturn tonumber(ARGV[2]);");
        lua.append("\nend");

        lua.append("\nc = redis.call('get',KEYS[1])");
        // 如果redis中值为原值,则设为新值
        lua.append("\nif c and tonumber(c) == tonumber(ARGV[1]) then");
        lua.append("\n redis.call('set',KEYS[1],tonumber(ARGV[2]))");
        lua.append("\nreturn tonumber(ARGV[2]);");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nreturn ;");
        return lua.toString();
    }

    /**
     * @description 编写Redis cas脚本
     */
    private String buildCompareAndSwapScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nlocal old ");
        lua.append("\nlocal new ");

        lua.append("\nc = redis.call('EXISTS',KEYS[1])");
        lua.append("\nif c and c==0 and tonumber(ARGV[1]) == nil then");
        lua.append("\n redis.call('set',KEYS[1],tonumber(ARGV[2]))");
        lua.append("\nreturn tonumber(ARGV[2]);");
        lua.append("\nend");

        lua.append("\nc = redis.call('get',KEYS[1])");
        // 如果redis中值为原值,则设为新值
        lua.append("\nif c and tonumber(c) == tonumber(ARGV[1]) then");
        lua.append("\n redis.call('set',KEYS[1],tonumber(ARGV[2]))");
        lua.append("\nreturn tonumber(ARGV[2]);");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nreturn ;");
        return lua.toString();
    }


    /**
     * @description 编写Redis 自增脚本
     */
    private String buildAutoIncrLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call('get',KEYS[1])");
        // 调用超过最大值,则直接返回
        lua.append("\nif c  then");
        lua.append("\nc = redis.call('incr',KEYS[1])");
        lua.append("\nelse");
        // 执行计算器自加
        lua.append("\nc = redis.call('set',KEYS[1],1)");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }

    public Long test(String key,Long oldValue,Long newValue){
        String luaScript = buildTestScript();
        key=wrapKey(myRedisConfig,key);
        ImmutableList<String> keys = ImmutableList.of( key);
        RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
        Number count = limitRedisTemplate.execute(redisScript, keys, oldValue,newValue);
//        if(count!=null && !"null".equals(count.toString())){
//            log.info("当前缓存内key is {}  值为={}", key, count);
//        }
        return  count==null?null:count.longValue();
    }



    public Long compareAndSwap(String key,Long oldValue,Long newValue){
        String luaScript = buildCompareAndSwapScript();
        key=wrapKey(myRedisConfig,key);
        ImmutableList<String> keys = ImmutableList.of( key);
        RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
        Number count = limitRedisTemplate.execute(redisScript, keys, oldValue,newValue);
        log.info("当前缓存内key is {}  值为={}", key, count);
        return  count==null?null:count.longValue();
    }

    public Long getAndIncrement(String key){
        String luaScript = buildAutoIncrLuaScript();
        RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
        key=wrapKey(myRedisConfig,key);
        ImmutableList<String> keys = ImmutableList.of( key);
        Number count = limitRedisTemplate.execute(redisScript, keys,1);
        log.info("当前缓存内key is {} for 值为={}", key, count);
        return count.longValue();
    }
}

这个是redis的lua的函数工具类,封装lua脚本的函数。里面主要有两个主要的方法,compareAndSwap,getAndIncrement。

getAndIncrement没啥用,就用来实现方案3的。主要还是看compareAndSwap。看名字也猜的出来,就是实现CAS(compare and swap)

https://blog.csdn.net/ln_6am/article/details/85642853

也是平平无奇的调用lua脚本,不过lua脚本的作用是,如果key不存在,而且old也是nil,则设置值为new。

如果key存在,且等于old值,则设置值为new。否则返回空。

@Override
public String getApplyNo() {
    Date date = new Date();
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");
    String now = simpleDateFormat.format(date);
    String key="OP2_"+now;



    Long applyNo =0L;
    Long nextApplyNo=1L;
    Long aLong = null;
    while(aLong==null){
        String applyNumber = redisUtil.get(key);
        if(applyNumber==null){
            aLong= redisLuaUtil.compareAndSwap(key,null,1L);
             continue;
        }else{
            applyNo=Long.valueOf(applyNumber);
            nextApplyNo=applyNo+1;
        }
        aLong = redisLuaUtil.compareAndSwap(key, applyNo, nextApplyNo);
    }


    return key+String.valueOf(aLong);
}

获取业务号的写法。

获取为空的话,可以从数据库中获取该月的id最大值,

这样的话,如果是新的月中redis的key被误删,会去取月中的id,或者是新增业务号,(就我这种情况,本来没有业务号的,redis肯定也没这个key的值的,那取数据库的最大id)

而每个月的话,你从数据库拿到的也是null,那也是从0开始

然后是测试,

@GetMapping("/f/test")
public Result test() {
    String applyNo = itoDeliverApplyService.getApplyNo();
    System.out.println("当前业务值为"+applyNo);
    return Result.ok(applyNo);
}

我用jmeter去测试,开了1000个线程去跑这个接口,结果都是返回980个业务号,左右,我本以为这个是我写错了,但是我不知道哪里写错了,然后就去看请求,发现有的请求是返回Could not return the resource to the pool。我估摸着是线程一直没拿到,然后一直在自旋,导致连接池不够的原因。

调大连接池后,如愿的获取到了1000个业务号。

从例子里也可以看出。这个方法吃reids的连接数,这就是CAS的通病,容易自旋,耗费资源,ABA的问题倒是不在这里考虑。

这个写法比较适合一些并发量不高的了。

方案5.同事说,既然不需要顺序递增,你每个请求去那1000个值,然后取里面的一个,那这样只要不是每秒1000个以上请求,那基本也不会有问题,

这个好像跟阿里的分布式id写法差不多,但是就不考虑了,因为这里是每个月的刷新,你弄每秒,7位数不够用吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值