在mysql函数中启动事物和行锁/悲观锁实现并发条件下获得唯一流水号

业务场景


  我有一个业务需求:我有一个报卡表 report里面有一个登记号字段 fcardno、地区代码 faddrno和发病年份 fyear,登记号由**“4位地区代码”+“00”+“发病年份”+“5位流水号”**组成,我要在每次插入一张报卡(每一行数据)的时候给每一张报卡生成唯一的登记号。
  登记号其他都好处理,主要是我们要怎么获取唯一的流水号,特别是并发情况。

解决方案1


  在不考虑并发的条件下,我可以随便写,我简单粗暴直接:

select count(*) +1 
from report 
where faddrno = {待插入行的faddrno} and fyear = {待插入行的fyear}

上面的查询结果就是我的流水号,然后前面填充0补齐5位就可以了。

解决方案2

  一开始由于经验不足,我写了一个函数f_getreportid_testJob并创建了一个表专门记录流水号的表ib_tbs_ib_tbs_tumorcardno_test
在这里插入图片描述

表里的数据大致是:
在这里插入图片描述
f_getreportid_testJob函数里做的是就是查流水号表如果表里该地区和年份有记录则将该行的fno+1作为当前报卡的流水号,然后更新流水号表该行的fno = fno +1;如果改表中没有该地区和年份的记录那么,就插一条fno=1的记录并且该报卡的流水号就为1。那么就可以初步实现为每个地区和年份记录连续的流水号了。f_getreportid_testJob代码如下:

CREATE DEFINER=`root`@`%` FUNCTION `f_getreportid_testJob`(`as_fyearno` VARCHAR(20),`as_faddrno` VARCHAR(20)) RETURNS varchar(200) CHARSET utf8mb4
    DETERMINISTIC
    COMMENT '获取上报卡编号函数'
BEGIN
		#市编码+00+年份+五位数字
  	DECLARE ls_fnum int DEFAULT 0;
		DECLARE ls_fcount int DEFAULT 0;#用来判断ib_tbs_ib_tbs_tumorcardno_test表中是否有数据
		DECLARE reportid varchar(255) DEFAULT ''; 
		
		if ifnull(as_fyearno,'') = '' or ifnull(as_faddrno,'') = '' then
				RETURN null;
		end if;
		
		select count(*) into ls_fcount
		from ib_tbs_ib_tbs_tumorcardno_test
		where fcityno = substr(as_faddrno,1,4)
		and fyearno = as_fyearno;
		
		if ls_fcount <> 0 then
				select fno + 1 into ls_fnum
				from ib_tbs_ib_tbs_tumorcardno_test
				where fcityno = substr(as_faddrno,1,4)
				and fyearno = as_fyearno;-- 获取流水号
				
				update ib_tbs_ib_tbs_tumorcardno_test set fno = fno + 1
				where fcityno = substr(as_faddrno,1,4)
				and fyearno = as_fyearno;-- 更新流水号
		else
				insert into ib_tbs_ib_tbs_tumorcardno_test(fcityno,fyearno,fno/*流水号*/)
				VALUES(substr(as_faddrno,1,4),as_fyearno,1);
				set ls_fnum = 1;
		end if;
		
		SET reportid = CONCAT(substr(as_faddrno,1,4),'00',as_fyearno,lpad(ls_fnum,5,'0'));
	RETURN reportid;
  END

问题暴露:
  在测试阶段没有发现问题,系统上线一段时间有发现有一些重复的登记号,简单说就是流水号重复了。查看每张报卡的插入时间是相同的:
在这里插入图片描述
  上面的函数中我们获取流水号的时候是先查(select)然后再将新的流水号更新(update)到流水号表,这两步操作在并发的情况下,两步操作之间就可能穿插其他其他用户的操作,比如其他用户的程序也在调用这个函数并且也刚好执行到更新同一行的fno操作,此时后者拿到的流水号与前者是相同的,那么就重复了。
  所以在并发情况下,我们还要拿到唯一的流水号就需要保证我查(select)当前流水号以及我取到一个新的流水号并更新(update)流水号fno的这两步操作是原子操作,即两步操作之间不能被其他程序查到同一行数据并更新同一行数据。

解决方案3-加锁


  再简化一下我们的目的:我们希望获取流水号和更新流水号之间,该行数据不被其他程序查询和更新流水号就能实现我整个报卡表的流水号都是唯一的。
  既然我们只需要关注流水号某一行的数据,那么我们自然可以想到加锁!因为我们对某一行先查然后更新,并且希望我在更新操作完成之后,其他程序才能查这一行的数据,这样就能保证我的流水号是唯一的了。
  在mysql中默认以自动提交模式运行,也就是说我在存储过程中操作数据库在存储过程执行完毕后不需要我们显示使用commit提交事物,mysql会自动替我们提交事物;你也可以使用 START TRANSACTION;手动开启一个事物,在事务结束的时候使用 COMMIT提交事务,或使用 ROLLBACK回滚事务。
  我们在原来的函数里面开启事物,并且使用行锁(或者叫悲观锁、排它锁。我在这里直接叫行锁是因为我查询和更新的时候都是可以通过fcityno和fyearno可以定位到唯一一行的,这两个字段是联合主键能确定唯一一行,所以在InnoDB引擎下它就是一个行级锁),注意 FOR UPDATE必须在事务的上下文中使用。如果没有开启事务,那么锁将没有意义,因为锁定会在每个单独的查询后自动释放。
  所以我们只要将上面的函数修改为存储过程并加一个OUT 参数用来记录生成的登记号,开启事物后,在获取流水号的sql语句结尾加上 FOR UPDATE;就可以了,代码可参考:

CREATE DEFINER=`root`@`%` PROCEDURE `p_getreportid_testJob`(`as_fyearno` VARCHAR(20),`as_faddrno` VARCHAR(20),OUT `as_rslt` varchar(500))
BEGIN
	#市编码+00+年份+五位数字
  	DECLARE ls_fnum int DEFAULT 0;
		DECLARE ls_fcount int DEFAULT 0;#用来判断ib_tbs_ib_tbs_tumorcardno_test表中是否有数据
		DECLARE reportid varchar(255) DEFAULT ''; 
		
		if ifnull(as_fyearno,'') = '' or ifnull(as_faddrno,'') = '' then
				set as_faddrno = null;
		
		else
		-- 开始事务  
			START TRANSACTION;
			
			select count(*) into ls_fcount
			from ib_tbs_ib_tbs_tumorcardno_test
			where fcityno = substr(as_faddrno,1,4)
			and fyearno = as_fyearno;
			
			if ls_fcount <> 0 then
					select fno + 1 into ls_fnum
					from ib_tbs_ib_tbs_tumorcardno_test
					where fcityno = substr(as_faddrno,1,4)
					and fyearno = as_fyearno
					FOR UPDATE; -- 使用FOR UPDATE锁定记录行
					
					update ib_tbs_ib_tbs_tumorcardno_test set fno = fno + 1
					where fcityno = substr(as_faddrno,1,4)
					and fyearno = as_fyearno;
			else
					insert into ib_tbs_ib_tbs_tumorcardno_test(fcityno,fyearno,fno/*流水号*/)
					VALUES(substr(as_faddrno,1,4),as_fyearno,1);
					set ls_fnum = 1;
			end if;
			
			COMMIT;-- 提交事务自动释放锁
			
			SET reportid = CONCAT(substr(as_faddrno,1,4),'00',as_fyearno,lpad(ls_fnum,5,'0'));
			
			set as_rslt = reportid;
		
		end if;
END

在函数里面使用行锁


  其实前面已经能实现我们的需求了,但是由于实际限制获取登记号的函数 f_getreportid_testJob函数我不能改为存储过程,我希望它还是一个函数,并且我并发的情况下一般都是使用存储过程调用这个函数,那我只需要在存储过程中调用这个获取登记号的函数前开启事物就可以了,然后 f_getreportid_testJob几乎不需要怎么修改:

CREATE DEFINER=`root`@`%` FUNCTION `f_getreportid_testJob`(`as_fyearno` VARCHAR(20),`as_faddrno` VARCHAR(20)) RETURNS varchar(200) CHARSET utf8mb4
    DETERMINISTIC
    COMMENT '获取上报卡编号函数'
BEGIN
		#市编码+00+年份+五位数字
  	DECLARE ls_fnum int DEFAULT 0;
		DECLARE ls_fcount int DEFAULT 0;#用来判断ib_tbs_ib_tbs_tumorcardno_test表中是否有数据
		DECLARE reportid varchar(255) DEFAULT ''; 
		
		if ifnull(as_fyearno,'') = '' or ifnull(as_faddrno,'') = '' then
				RETURN null;
		end if;
		
		
		select count(*) into ls_fcount
		from ib_tbs_ib_tbs_tumorcardno_test
		where fcityno = substr(as_faddrno,1,4)
		and fyearno = as_fyearno;
		
		if ls_fcount <> 0 then
				select fno + 1 into ls_fnum
				from ib_tbs_ib_tbs_tumorcardno_test
				where fcityno = substr(as_faddrno,1,4)
				and fyearno = as_fyearno
				FOR UPDATE; -- 使用FOR UPDATE锁定记录行,只加这一个
				
				update ib_tbs_ib_tbs_tumorcardno_test set fno = fno + 1
				where fcityno = substr(as_faddrno,1,4)
				and fyearno = as_fyearno;
		else
				insert into ib_tbs_ib_tbs_tumorcardno_test(fcityno,fyearno,fno/*流水号*/)
				VALUES(substr(as_faddrno,1,4),as_fyearno,1);
				set ls_fnum = 1;
		end if;
		
		SET reportid = CONCAT(substr(as_faddrno,1,4),'00',as_fyearno,lpad(ls_fnum,5,'0'));
	RETURN reportid;
  END

  在存储过程中就按下面的代码进行调用:

CREATE DEFINER=`root`@`%` PROCEDURE `p_call_getreportid_testJob`()
BEGIN
	# 模拟一个存储过程调用f_getreportid_testJob获取函数
	# 将获取的流水号插入到q3表
	
	START TRANSACTION;-- 开始事务 
	
	insert into q3(c1)
	select f_getreportid_testJob('2023','4404');-- 获取流水号
	
	COMMIT;-- 提交事务自动释放锁
	
END

执行完后q3.c1就有值:440400202316430
在这里插入图片描述
  需要注意的是,如果外层的存储过程的处理逻辑比较多,甚至你还开启了循环,所以你最好尽量在调用f_getreportid_testJob函数的附近(最好就是前一行)开启事物,因为START TRANSACTION; 和下一个COMMIT;之间的所有代码都被视为原子操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/556702.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MATLAB源码-第46期】基于matlab的OFDM系统多径数目对比,有无CP(循环前缀)对比,有无信道均衡对比。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 OFDM&#xff08;正交频分复用&#xff09;是一种频域上的多载波调制技术&#xff0c;经常用于高速数据通信中。以下是关于多径数目、有无CP&#xff08;循环前缀&#xff09;以及有无信道均衡在OFDM系统中对误码率的影响&am…

自如电费均摊问题

3月份搬了次家&#xff0c;嫌麻烦租了自如&#xff0c;第一个月的电费账单出来了&#xff0c;由于我是中途搬进去的&#xff0c;于是乎就好奇他会如何计算均摊&#xff0c;这个月电费账单出来了&#xff0c;算了下发现了点东西。 先说结论&#xff1a;按照我的这个均摊的方式&a…

刻度清晰耐酸碱腐蚀PFA材质实验室用塑料量具特氟龙量筒量杯

PFA量筒为上下等粗的直筒状&#xff0c;特氟龙量杯是上大下小的圆台形&#xff0c;底座均有宽台设计&#xff0c;保证稳定性&#xff0c;两者均可在实验室中作为定量量取液体的量具&#xff0c;上沿一侧有弧嘴设计&#xff0c;便于流畅地倾倒液体。 规格参考&#xff1a;5ml、…

主线程捕获子线程异常

正常情况下使用多线程出现异常时&#xff0c;都是按照单个任务去处理异常&#xff0c;在线程间不需要通信的情况下&#xff0c;任务之间互不影响&#xff0c;主线程也不必知道子线程是否发成异常。 那么只需要处理子线程异常即可 Task.Run(() > {try{throw new Exception(&…

【Vision Pro应用】分享一个收集Apple Vision Pro 应用的网站

您是否也觉得 Vision Pro 应用程序商店经常一遍又一遍地展示相同的几个 VisionOS 应用程序?许多有趣、好玩的应用程序似乎消失得无影无踪,让人很难发现它们。为了帮助大家更轻松地探索和体验最新、最有趣的 Vision Pro 应用程序,这里分享一个网站https://www.findvisionapp.…

IDEA @Autowired不显示红线

IDEA 中&#xff0c;Autowired 显示红线一般情况是注入 Mapper 或者 Dao 时出现的&#xff0c;如下图&#xff1a; 这个报错是因为 Mapper 或者 Dao 上没有加 Repository 或者 Mapper&#xff0c;Autowired 注入时就判断为这不是一个 Bean。 不建议通过加上面两个注解的方式解…

Java面试八股之hashCode()和equals()方法的重要性

hashCode()和equals()方法的重要性 逻辑判断&#xff1a;equals()方法用于定义对象逻辑上的相等标准&#xff0c;即当两个对象在业务意义上被视为“相同”时&#xff0c;equals()应返回true。 哈希表支持&#xff1a;hashCode()返回一个整数哈希码&#xff0c;用于在哈希表&a…

【电路笔记】-数字逻辑门总结

数字逻辑门总结 文章目录 数字逻辑门总结1、概述2、逻辑门真值表3、总结 数字逻辑门有三种基本类型&#xff1a;与门、或门和非门。 1、概述 我们还看到&#xff0c;数字逻辑门具有与其相反或互补的形式&#xff0c;分别为“与非门”、“或非门”和“缓冲器”&#xff0c;并且…

AIGC的崛起:定义未来内容创作的新纪元

&#x1f31f;文章目录 &#x1f31f;AIGC简介&#x1f31f; AIGC的相关技术与特点&#x1f31f;AIGC有哪些应用场景&#xff1f;&#x1f31f;AIGC对其他行业影响&#x1f31f;面临的挑战与问题&#x1f31f;AIGC未来发展 &#x1f31f;AIGC十大热门网站推荐&#xff1a; 文心…

常用的7个免费ChatGPT网站

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

2022天梯赛 L3_2 关于深度优先搜索和逆序对的题应该不会很难吧这件事 【树上逆序对计数】

传送门&#xff1a;https://pintia.cn/problem-sets/994805046380707840/exam/problems/1518582895035215872?type7&page1 思路 观察发现&#xff0c;逆序对可以分成两类&#xff1a; 节点 u u u 和 v v v 有明确的父子关系&#xff08;不一定是直属的直连边&#xff…

今天给大家推荐36套404页面模板

404页面是网站必备的一个页面&#xff0c;它承载着用户体验与SEO优化的重任。当用户访问不存在的页面时&#xff0c;服务器会返回404错误代码&#xff0c;并显示404页面。一个好的404页面可以帮助用户快速找到所需信息&#xff0c;并提升网站的用户体验。 以下是一些演示下载资…

Web3技术简介:重新定义互联网的未来

引言 在21世纪的数字时代&#xff0c;互联网已成为我们日常生活的不可或缺的一部分。然而&#xff0c;随着区块链和加密技术的快速发展&#xff0c;一个全新的互联网模型——Web3&#xff0c;正逐渐崭露头角。Web3不仅仅是技术的进步&#xff0c;它更是对传统互联网模型的挑战…

类和对象中阶3⃣️-默认成员函数(赋值运算符重载,取地址及 const取地址操作符重载等)

目录 5.赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 5.4 重载流插入与流提取 流插入<<运算符重载 流提取运算符重载 6.日期类实现 7.const成员 8.取地址 及 const取地址操作符 重载 5.赋值运算符重载 5.1 运算符重载 C为了增强代码…

数仓建模—数据仓库初识

数仓建模—数据仓库初识 数据仓库之父Bill Inmon在1991年出版的"Building the Data Warehouse"一书中所提出的定义被广泛接受 数据仓库&#xff08;Data Warehouse&#xff09;是一个面向主题的&#xff08;Subject Oriented&#xff09;、集成的&#xff08;Integ…

Mysql索引专题

文章目录 1. 数据库索引结构1.1 Hash结构1.2 树结构1.3 Mysql索引怎么提升效率? 2. 执行计划 explainidselect_typetabletypepossible_keyskeykey_lenrefrowsfiteredextra 1. 数据库索引结构 我们都知道mysql数据库的常用存储结构是B树&#xff0c;为什么是B树&#xff1f;试…

邮件代发API发送邮件如何使用?操作指南?

邮件代发邮箱API发送邮件的步骤&#xff1f;代发有哪些注意事项&#xff1f; 在自动化办公、批量营销等场景中&#xff0c;手动发送邮件往往显得效率低下&#xff0c;这时候&#xff0c;邮件代发API就显得尤为重要。那么&#xff0c;邮件代发API发送邮件究竟如何使用呢&#x…

买婴儿洗衣机怎么选择?四大绝佳好用婴儿洗衣机分享

幼龄时期的宝宝的衣物&#xff0c;是比较需要注意的时候。可能一不注意宝宝穿在身上就会有不适宜症状发生。所以宝妈们真的要随时观察&#xff0c;然后在宝宝洗衣服的这上面多下点功夫&#xff0c;不要让宝宝受到这种无谓的伤害。小婴儿的抵抗力比我们差很多。有些细菌、病毒可…

Hadoop大数据处理技术-Linux相关命令

​7.Linux常用命令 1&#xff09;Windows中的dir&#xff1a;列出当前目录下所有的文件和目录 2&#xff09;cd&#xff1a;改变当前目录 cd命令并不能直接实现这种跳跃转换目录的功能 它只能让你在当前目录和其子目录之间来回切换 就像在一张平面地图上移动一样 如果想跨目录…

如何挑选护眼灯?分享护眼灯排行榜前十名

许多家长肯定都有这样的烦恼&#xff0c;家中的孩子自从上学后&#xff0c;每天回家后的学习作写阅读时总会在不知不觉间越来越贴近书本&#xff0c;后来还会时不时眯着眼睛看东西&#xff0c;但其实这种用眼习惯的最大原因是孩子没有足够光线和舒适的环境光线导致的&#xff0…