流水号(自动编号)生成相关问题(并发、事务)及解决方案

本文探讨了在多用户环境中生成流水号时遇到的并发问题及其解决方案,包括使用分布式锁、事务管理和数据库操作技巧等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

流水号生成的事务和并发问题

因为界面可能多个人创建了表单,生成的流水号号需要顺序加:

//从数据库中取出上一次生成的流水号编号
number = select value from 流水号表 where id =//更新数据库 编号+1
update number = number + 1;
return number;

这里就会有并发问题:
假如有2个人同时去数据库取值 , 取到的值可能是一样的, 所以生成的 编号也可能一样 ,这就有问题了

并发问题产生的原因和解决方案

为什么会有并发问题
最主要问题就是有 **共享数据 **
从数据库里面取数据 , 或者操作一个程序外面定义的值 ,都会产生并发问题

private  int num = 0;
public void fun(){
	num = num + 1;
}

如上面的程序,有50个程序同时进入函数 ,那结果num的值肯定不是0
有共享数据的地方就会有并发问题,
但是并发问题对数据的影响,我们不一定要关系,假如说界面用户要修改一个值,同时有很多用户修改,这是时候当然有并发问题,但是最后修改结果是什么,可能是谁提交的快,数据库对这里也有控制顺序,
最后的结果,谁也不知道,但是这不重要,如果是订单什么秒杀系统,这里的值就变得重要。(理解一下实际的场景
这里的编号生成就是重要的,因为他不能重复,严格要求的话,还必须是连续的,中间不能有断号。

这里在说一下 多线程和并发的关系,多线程就相当于很多用户操作了,还是程序里面如果有共享数据,并发问题就存在。
多线程可能是并发执行,并行执行,这里要看 cpu的核心数或者cpu数,目前的CPU都是多核心单CPU,很少有多CPU每个CPU多个核心(我不知道是不是很少,个人PC都是前面的情况)
并发执行 其实就是 一段时间内 看起来像是 并行 ,并行就是真的 一起执行。
所以并行执行 和 并发产生的问题是2个概念,并行也有并发问题。
我可以这样说 只要同时操作共享数据就有可能问题。
接下来 并发问题如何解决,加锁

private  int num = 0;
public synchronized void fun(){
	num = num + 1;
}

加上 synchronized 锁,是最简单的方式 ,还有很多 java的 锁 这个后面再说 ,先说另一个问题 事务

为什么有事务

先说这里代码为什么牵扯到事务
因为我要操作2张表,不仅仅是要更新这个流水号表,还要insert我这条记录

1. update number +12. insert 前缀+年号+number+后缀 into 记录表

比如说OA系统,有人申请一个报销单,报销单的编号肯定是要存一个表的
所以这里的 批量操作 就需要 有事务 去保持操作的原子性
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
比如我这里 更新了编号 , 但是可能插入失败了,就需要取消编号+1的更新(回滚
事务有ACID四个特性,最主要的就是保证原子性。
数据库同时还面临着并发问题,事务的隔离性就是处理各种并发问题的。

事务+并发锁 产生的问题

刚刚说了这里生成流水号必须要有事务,spring处理事务也比较简单,声明式事务就是一个 @Transactional 注解

@Transactional
public void fun(){
    1. update number +12. insert 前缀+年号+number+后缀 into 记录表
}

同时这里还需要有并发处理加锁

@Transactional
public synchronized void fun(){
    1. update number +12. insert 前缀+年号+number+后缀 into 记录表
}

看起来没有什么问题 ,但是这里存在一个问题

  1. 锁失效:

spring的事务处理通过AOP实现的,AOP的原理是动态代理,动态代理是反射调用接口,接口相当于没有同步加锁,锁失效了。
这里不是没有加锁,而是锁的范围
Synchronized 失效关键原因:是因为Synchronized锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内。出现同步锁失效的原因是:当A(线程) 执行完getSn()方法,会进行释放同步锁,去做提交事务,但在A(线程)还没有提交完事务之前,B(线程)进行执行getSn() 方法,执行完毕之后和A(线程)一起提交事务, 这时候就会出现线程安全问题。
那怎么办呢?
问同事,同事说用 redis分布式锁 ,这里用 redisson实现的锁:

public String getAutonumber(Long fieldId) {
        //根据fieldId 查找 表
        RLock lock = redissonClient.getLock(String.valueOf(fieldId))StringBuffer autonumberString = new StringBuffer(100);
        try {
            // 最多加锁三分钟
            boolean tryLock = lock.tryLock(3L, 30L, TimeUnit.SECONDS);
            if(tryLock){
                CustomFieldAutonumber autonumber = customFieldAutonumberDao.queryCustomFieldAutonumberByFieldId(fieldId);
                Long presentValue = autonumber.getPresentValue();
                autonumberString.append(autonumber.getPrefix())
                        .append("2021")
                        .append(presentValue)
                        .append(autonumber.getSuffix());
                autonumber.setPresentValue(presentValue + autonumber.getStep());
                autonumber.update();
            }else{
                System.out.println("未获取到锁");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return autonumberString.toString();

看起来还是没有问题,但是测试并发却发现还是用重复的,很奇怪为什么锁还是失效了
然后把数据库的操作注释了,改成 就操作一个数字 ,发现锁其实是 生效的 ,
所以猜测 update 是不是异步操作,但是看了源码 (这里用的框架其实是jfinal )并没有异步。
搞了半天最后找到了 ,
因为MYSQL 默认隔离级别是 可重复读 。
多个事务操作,只是获取编号加了锁,其实 事务没有结束 ,别的事务又进来了,锁释放了,所以他也去获取编号 ,可重复读,他并不能获取到前者的提交

上面说的也不对。。。
其实还是那个情况 AOP让锁失效了
所以解决方案有几种 :

  1. ~~锁 操作 全部的代码 ~~必须是分布式锁啊 ** **

其实是不行的,因为AOP 要好好理解一下

  1. 使用 别的 操作来 更新编号 不用锁
    1. 数据库 唯一索引 (不太好 ,因为如果重复,代码会报错
    2. 存储过程 函数 (可以 ,但是没有深入研究是怎么写的 后面可以继续
  2. **使用redis存储 redis直接原子更新 在同步sql ** (这个可以 但是redis挂了就没法了)
  3. 不用事务 我其实最后采用的是这个 因为 我这里 允许 insert 失败 , 编号 会 跳号 并不要求那么严格 (目前情况
  4. 下面这个 可能是 我最后的实现方案 ,但是并发量大 的话 会很慢

最后一个重要知识点 我可以 锁住事务,那么锁也不会失效

public synchronized void fun(){
    sqlfun();    
}
@Transactional
    public void sqlfun(){
        1. update number +12. insert 前缀+年号+number+后缀 into 记录表
    }

还是看起来没有什么问题 ,但是这里 事务失效了。。。。。。
为什么呢?

流水号的定义规则如何生成

  1. 年号的判断

考虑年号规则可以是 年 年月 年月日 生成不同 String

Date date = new Date();SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyyMMdd"); //20210919
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyyMM");   //202109
SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("yyyyM");	   //2021
String format = simpleDateFormat.format(date);
  1. 数字补0
//控制补4位 4 - 0004  12- 0012
String.format("%04d",num);
//代码控制补 几位
String.format("%0"+step+"d",num);
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值