流水号生成的事务和并发问题
因为界面可能多个人创建了表单,生成的流水号号需要顺序加:
//从数据库中取出上一次生成的流水号编号
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 +1;
2. insert 前缀+年号+number+后缀 into 记录表
比如说OA系统,有人申请一个报销单,报销单的编号肯定是要存一个表的
所以这里的 批量操作 就需要 有事务 去保持操作的原子性
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
比如我这里 更新了编号 , 但是可能插入失败了,就需要取消编号+1的更新(回滚)
事务有ACID四个特性,最主要的就是保证原子性。
数据库同时还面临着并发问题,事务的隔离性就是处理各种并发问题的。
事务+并发锁 产生的问题
刚刚说了这里生成流水号必须要有事务,spring处理事务也比较简单,声明式事务就是一个 @Transactional 注解
@Transactional
public void fun(){
1. update number +1;
2. insert 前缀+年号+number+后缀 into 记录表
}
同时这里还需要有并发处理加锁
@Transactional
public synchronized void fun(){
1. update number +1;
2. insert 前缀+年号+number+后缀 into 记录表
}
看起来没有什么问题 ,但是这里存在一个问题
- 锁失效:
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让锁失效了
所以解决方案有几种 :
- ~~锁 操作 全部的代码 ~~
必须是分布式锁啊** **
其实是不行的,因为AOP 要好好理解一下
- 使用 别的 操作来 更新编号 不用锁
- 数据库 唯一索引 (不太好 ,因为如果重复,代码会报错)
- 存储过程 函数 (可以 ,但是没有深入研究是怎么写的 后面可以继续)
- **使用redis存储 redis直接原子更新 在同步sql ** (这个可以 但是redis挂了就没法了)
- 不用事务 我其实最后采用的是这个 因为 我这里 允许 insert 失败 , 编号 会 跳号 并不要求那么严格 (目前情况
- 下面这个 可能是 我最后的实现方案 ,但是并发量大 的话 会很慢
最后一个重要知识点 我可以 锁住事务,那么锁也不会失效
public synchronized void fun(){
sqlfun();
}
@Transactional
public void sqlfun(){
1. update number +1;
2. insert 前缀+年号+number+后缀 into 记录表
}
还是看起来没有什么问题 ,但是这里 事务失效了。。。。。。
为什么呢?
流水号的定义规则如何生成
- 年号的判断
考虑年号规则可以是 年 年月 年月日 生成不同 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);
- 数字补0
//控制补4位 4 - 0004 12- 0012
String.format("%04d",num);
//代码控制补 几位
String.format("%0"+step+"d",num);