填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题

  • 背景描述

某日上午生产上突然出现应用无法连接数据库,c3p0错误connect time out,重启应用后依然不见好转,经DBA检查发现存在对某张表的for update,以及其他业务操作对该表的update操作,且这些会话均长时间未释放,查看日志也发现这些sql语句执行时间有些甚至长达100多秒,后通过DBA 手工删除会话,释放连接后系统才恢复正常,导致此事故的是系统的某一个业务功能,但却因为这个不当操作导致系统全线业务瘫痪,由于此前该功能已经运行多日,却未发现异常。

  • 代码检查

这里先贴一段代码,由于代码是在前任挖坑离职后,我后面接过来的,大家自行体会。

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第1张图片

这段代码目的是先锁住整表,然后查出主键的maxvalue,然后根据规则对maxvalue进行+1 ,然后进行insert操作,在没请求量,不对表做update操作的情况下,单次执行确实是没什么问题,但是问题就在这张表是业务表,是会对数据进行操作的,一旦加上存在并发,哪怕这个并发都不是秒级的,这种操作都是撑不住的,关键!做这个操作的表还不止一张!其中有几张还是比较核心的业务表,每个表的主键都是通过这种方式来搞,当时的感觉就仿佛吃了一口老八秘制小汉堡。

DBA杀掉会话临时解决后,下午又出现了同样的情况,没办法,完全修复需要时间,只能先紧急对sql进行修改 select * from table for update wait 3 ,也就是本次for update 若3s未获得锁,则直接返回资源忙碌,虽然会造成前端的部分请求失败,但至少不会导致系统全线崩溃,然后紧急上线,后未发生同样的情况。

  • 代码修改

知道了问题,那就定方案吧,第一时间想到的是将序号增加操作表和业务表分开,单独建立一张序号表,由于业务需求是对序号每日从1开始递增+1,可以以日期做主键,和不同的表类型做主键,来进行行级锁,但是无论是从代码改动量还是性能上来说,都不是很合适,于是采用了oracle sequence 自增的方案,由于本业务的并发并不高,所以性能上应该是够了,下面是修改过后的代码(非正式投产版)。

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第2张图片

通过创建序列,每次只需要获得序列的下一个值,保证了主键唯一性,然后再进行业务规则拼接,再对业务表进行插入,代码写好了,接下来进行并发性能测试。

  • 测试代码

由于代码是有公司框架封装的,根据思路理解就好

      1.首先创建线程类,执行方法单元测试。

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第3张图片

      2.创建线程池,模拟500并发情况下,是否会执行失败或者序号冲突,接下来开始执行。

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第4张图片

      3.控制台打印

可以看到线程无序执行

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第5张图片填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第6张图片

500线程并发执行时间不超过3s,由于多个请求在执行序列,出现部分线程执行序列时间在1-2s之间,但考虑到是本地运行测试,所以在可接受范围,此时查看业务表的数据库业务编号

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第7张图片

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第8张图片

可以看到序号从1开始到500结束,count查看条数,500条数据无误,执行过程中未发生错误,基本上可以认为通过这种方式解决了本次出现的问题。

  • 剩下的问题

代码层面的问题通过上面我们可以认为是解决了,但是依然有个问题,业务规则要求业务序号每日从1开始递增,我们创建的序列却是一直递增的,这样明显不行;

于是决定通过每日凌晨重置序列(业务在凌晨时不会发生新增)的方式来达到业务需求,具体实现如下:

     一、创建oracle存储过程

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第9张图片

这个存储过程是指在执行时

1.首先拿到当前的下一个序列号,然后赋值给变量n

2.修改sequence增长数值为-n

3.再次执行sequence.nextval,此时sequence序列号变为0;

4.再次修改sequence增长数为1;

经过这个过程后当执行select sequence.nextval from dual 时,就会从1开始了。

     二、创建oracle自动任务job

存储过程创建完成后,我们需要让它能够自动执行,不可能每天晚上人工执行,于是通过下面的方式让oralce每晚0点来替我们自动执行存储过程,在测试验证时可以先将执行间隔调整为30s或者更短。

填坑指南:一次通过Oracle序列自增解决业务编号唯一的并发问题_第10张图片

经过验证发现Oracle自动任务帮我们自动完成了序列每日重置为1 操作,这样一来,我们的问题就得到了一个相对完整的解决方案了。

但是转念一想还是有风险,若今天为29号当前序号已经到了20,29号 23:59:59:999接受了一笔业务取到当前序号为21,存储过程即便自动执行了,但这笔业务在30号00:00:00:001的时候执行了业务表插入操作,序号为202005300001;这sequence的currval已经是0,当下一笔业务到来时,取到nextval就成了1,那这时就出现了两笔202005300001,第二笔进行insert肯定会导致主键冲突,或者业务错误。

这种情况其实是很极端了,第一当前业务产经凌晨基本上不会有数据请求,但是为了避免风险,还是和业务部门商量在前端页面增加23:59:30-00:00:30不接受提交的控制,若业务场景是必须保持24小时不间断服务,而且请求量很大的话,可能本方案就要重新进行设计了

  • 总结

引发本次生产事故的直接原因是对整表,而且是会频繁update的业务表进行了全表for update,开发时的思考不够缜密,这是其一;其二是进行代码审核时不够仔细没有及时发现并提出问题;其三是未对新增业务进行必要的压力测试;针对for update语句,以后需要明令禁止投入生产,即便是逼不得已的情况下必须使用for update,也要尽可能的指定表中的某一条或少量的数据,并且增加wait条件及时释放。

填坑不容易,且填且珍惜!

 

你可能感兴趣的:(填坑指南,数据库,oracle,java)