分布式锁和事务关系的细节

news2024/11/27 6:29:17

使用redssion在redis上以及结合自定义注解利用spring的环绕切面来实现分布式锁功能

代码示例

controller、service层

@RequestMapping("insertNumber/{number}/{id}")
public boolean insertNumber(@PathVariable Long number,@PathVariable Long id){
    return testService.insertNumber(number,id);
}
@Transactional
@ServiceLock(name = "insertNumber",keys = {"#id"},waitTime = 50)
public boolean insertNumber(final Long number, final Long id) {
    Test test = testMapper.getById(id);
    Long originalNumber = test.getNumber();
    originalNumber = originalNumber + number;
    test.setNumber(originalNumber);
    testMapper.updateById(test);
    return true;
}

逻辑很简单,根据id查出test对象后,在原有基础上增加number值,然后更新到数据库中。

锁的切面

@Aspect
public class ServiceLockAspect {

    @Around("@annotation(servicelock)")
    public Object around(ProceedingJoinPoint joinPoint, ServiceLock servicelock) throws Throwable {
        //进行加锁
        boolean reuslt = lock.lock(lockName);
        //如果加锁成功
        if (reuslt) {
            try {
                //执行业务逻辑
                return joinPoint.proceed();
            }finally{
                //解锁
                lock.unlock(lockName);
            }
        }else {
            //等待或者执行加锁失败的处理逻辑
        }
    }
}

但是在实际压测中发现并不能保证数据的正确性,比如设置一秒内发出100个请求,参数number为1,正确结果应该是100,但实际要比100小。

原因

这是一个很细节的问题,如果在service方法上加锁,事务本质上也是切面,这时也就是该方法上同时存在锁的切面和事务的切面,spring会将事务的切面和锁的切面放在一个切面有序集合中,然后依次的执行,这其实也是责任链模式。

而在执行顺序中是有讲究的,如果采用上述代码,那么执行的顺序就是 1事务 2锁

执行流程图

切面的默认加载顺序 AspectJAwareAdvisorAutoProxyCreator#sortAdvisors

切面默认加载顺序说明

这个顺序是怎么确定的呢,就要看下事务切面和锁切面都是在什么时候放在链路集合中的

AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
  //查找切面
  List<Advisor> candidateAdvisors = findCandidateAdvisors();
  List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
  extendAdvisors(eligibleAdvisors);
  if (!eligibleAdvisors.isEmpty()) {
    //将切面集合进行排序
    eligibleAdvisors = sortAdvisors(eligibleAdvisors);
  }
  return eligibleAdvisors;
}

AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

protected List<Advisor> findCandidateAdvisors() {
  //这里从父类找到了事务切面,并放入到advisors中
  List<Advisor> advisors = super.findCandidateAdvisors();
  // Build Advisors for all AspectJ aspects in the bean factory.
  if (this.aspectJAdvisorsBuilder != null) {
    //这里找到了锁的切面,放入到advisors
    advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
  }
  return advisors;
}

可以看到是先是父类找到了事务的切面放到了advisors集合中,后来又找到了锁的切面也放到了advisors集合中。

后续的sortAdvisors方法是针对@order值来排序,而事务和锁的切面都为Integer.MAX_VALUE,分析一下排序逻辑

org.aspectj.util.PartialOrder#sort

/**
 * @param objects must all implement PartialComparable
 * 
 * @returns the same members as objects, but sorted according to their partial order. returns null if the objects are cyclical
 * 
 */
public static List sort(List objects) {
	// lists of size 0 or 1 don't need any sorting
	if (objects.size() < 2) {//一个的话,不用排序,直接返回
		return objects;
	}

	// ??? we might want to optimize a few other cases of small size

	// ??? I don't like creating this data structure, but it does give good
	// ??? separation of concerns.
    // 这里上边解释了半天,是他不想构造这个数据结构,但是又觉得这个数据结构可以分离很多复杂的逻辑
    // 下边这个方法是构造了一个SortObject,将advisors列表中每个元素,都用SortObject包装一下,包装后,里面会保存比当前这个advisor大的元素有几个,小的有几个,这样两个列表,后边的逻辑中会根据这两个列表中的值,进行具体的排序比较
	List<SortObject> sortList = new LinkedList<SortObject>(); // objects.size());
	for (Iterator i = objects.iterator(); i.hasNext();) {
		addNewPartialComparable(sortList, (PartialComparable) i.next());//将advisor包装成SortObject,并加入sortList
	}

	// System.out.println(sortList);

	// now we have built our directed graph
	// use a simple sort algorithm from here
	// can increase efficiency later
	// List ret = new ArrayList(objects.size());
	final int N = objects.size();
	//下边会进行两次嵌套的遍历,从sortList中选出最小的,放入objects中
	for (int index = 0; index < N; index++) {
		// System.out.println(sortList);
		// System.out.println("-->" + ret);

		SortObject leastWithNoSmallers = null;

		for (Iterator i = sortList.iterator(); i.hasNext();) {
			SortObject so = (SortObject) i.next();
			// System.out.println(so);
			//判断有无更小的对象,如果没有,则当前的对象为最小
			if (so.hasNoSmallerObjects()) {
				if (leastWithNoSmallers == null || 
					//fallbackCompareTo总会返回0
					so.object.fallbackCompareTo(leastWithNoSmallers.object) < 0) {
					leastWithNoSmallers = so;
				}
			}
		}

		if (leastWithNoSmallers == null) {
			return null;
		}
		//从sortList中移除最小的对象,这个会遍历sortList中的所有对象,从各个对象保存比自己小的对象的列表中移除掉
		removeFromGraph(sortList, leastWithNoSmallers);
		//从SortObject中取出advisor,放入objects列表中
		objects.set(index, leastWithNoSmallers.object);
	}

	return objects;
}
  • 初始化时,将所有切面加载到一个域成员变量的Map缓存中,加载时会将每个切面类中的切面方法进行排序
  • 切面方法中的排序方式,首先根据切面注解触发的顺序排序,然后根据字母序进行排序
  • 初始化完成后,每个切面类中的切面方法的顺序就不会再次改变了
  • 每次调用切面命中的业务代码时,会触发切面扫描,筛选出匹配的切面方法,根据切面方法所在的切面类,通过order属性的值,做一次排序,这次排序不会更改之前同一个类型中切面方法的相对顺序
  • 根据上边几步的排序结果,依次触发切面的逻辑

上面分析的排序是指业务切面之间的排序逻辑,但是当业务切面和事务切面都存在的话,如果不指定order的值,那么事务切面的执行顺序始终都会先于业务切面,不会按照切面名字来排序。

事务切面和aTestAspect切面

事务切面和zTestAspect切面

可以看到事务切面始终都在业务切面先执行

修改源码复现问题

通过上述源码级别的分析我们知道了问题就在开启事务和提交事务这部分,因为锁是在事务里面,开始事务和提交事务部分是没有被锁住的。

为了能更好的压测出问题,我通过修改Spring事务切面的源码,在执行业务逻辑和提交事务中间的这块加上休眠时间

事务切面 TransactionAspectSupport

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {

    // If the transaction attribute is null, the method is non-transactional.
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    final TransactionManager tm = determineTransactionManager(txAttr);


    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 开启事务
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            //执行业务逻辑
            retVal = invocation.proceedWithInvocation();
            //休眠200ms
            Thread.sleep(200);
        }
        catch (Throwable ex) {
            //回滚事务
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            cleanupTransactionInfo(txInfo);
        }
        //提交事务
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

发现确实压测每秒100的请求,每次压测数据都不能保证正确性

解决

既然知道了原因,那么解决办法就是将锁放到事务外,保证整个事务也被锁住即可解决

流程图

那么怎么样才能够让锁的切面放到事务切面外呢,答案就是使用@order注解,让锁的切面的顺序先于事务,那么@order的值设置为多少合适呢,事务的order值默认为Integer.MAX_VALUE,考虑到后续可能还要用到切面功能,也需要在锁切面的里面,所以这里我设置为-10

@Aspect
@Order(-10)
public class ServiceLockAspect {
    //省略
}

使用@order后切面的加载顺序

AspectJAwareAdvisorAutoProxyCreator#sortAdvisors

可以看到使用@order后切面的顺序达到了我们想要的效果。

经过多次压测后,数据确实保证了正确性

总结

由于事务和业务切面的执行顺序问题导致了锁的范围没有将整个事务包裹住,解决方案:

  1. 将锁的切面放在controller的方法上,这样锁的切面肯定会先于事务切面执行
  2. 如果锁的切面和事务切面在一个方法上,那么指定锁切面的order值,比事务切面order值小即可(事务切面order默认为Integer.MAX_VALUE)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

rust 中protobuf生成与使用

首先创建一个项目proto 进入到这个文件夹中 创建我们的proto文件 初始化的项目结构是这个样子的 新建一个hello.proto文件内容如下 syntax "proto3";package hello;service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {} }message HelloRequest …

干货 | 师兄手把手教你如何踏上科研道路

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐&#xff5e; 今天&#xff0c;邀请到鲁小白&#xff0c;给大家分享一下他踏上科研道路的心路历程。 大家好&#xff0c;我是鲁小白&#xff0c;我真正进入科研的时间&#xff0c;研究生3年再…

【C++】类和对象——类的引入、类的访问限定符、类的作用域、类的实例化、类的储存、this指针的引出和特性

文章目录 1.类的引入2.类的访问限定符3.类的作用域4.类的实例化5.类的储存6.this指针6.1this指针的引出6.2this指针的特性 1.类的引入 C是在C的基础上加以扩展。 在C语言中&#xff0c;我们想要让一个类型含有多种成员变量&#xff0c;我们使用结构体&#xff1b;而在C中我们可…

Doris节点扩容及数据表

扩容和缩容 上篇文章简单讲了doris的安装&#xff0c;本章分享的是doris中fe和be节点的扩容缩容以及doris的数据表1、FE 扩容和缩容 使用 MySQL 登录客户端后&#xff0c;可以使用 sql 命令查看 FE 状态&#xff0c;目前就一台 FE mysql -h linux -P 9030 -uroot -p mysql&…

python+django乡村居民数据的可视化平台

本论文主要论述了如何使用Django框架开发一个乡村振兴数据的可视化平台 &#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述乡村振兴数据的可视化平台的当前背景以…

拼多多二面,原来是我对自动化测试的理解太浅了

如果你入职一家新的公司&#xff0c;领导让你开展自动化测试&#xff0c;作为一个新人&#xff0c;你肯定会手忙脚乱&#xff0c;你会如何落地自动化测试呢&#xff1f; 01 什么是自动化 有很多人做了很长时间的自动化但却连自动化的概念都不清楚&#xff0c;这样的人也是很悲…

Android之 MVC到MVVM架构发展和封装

一 简介 1.1 软件架构发展趋势是解耦&#xff0c;即分离数据层和视图层&#xff0c;使得数据层专注于业务的数据和逻辑处理。从而提高代码的可读可编辑效率&#xff0c;提高团队协作能力&#xff0c;项目的生产能力&#xff0c;降低后期维护成本。 1.2 Android架构发展MVC -…

计算机组成原理实验四 微程序控制器实验报告

我班算是几乎最后一个做实验的班级了&#xff0c;报告参考了一些朋友提供的数据加上一些自己的主观拙见&#xff0c;本人水平有限加之制作仓促难免有错误&#xff0c;望大家批评指正。 4.1 微程序控制器实验 一、实验目的 (1) 掌握微程序控制器的组成原理。 (2) 掌握微程序的…

【蓝桥杯计算思维题】少儿编程 蓝桥杯青少组计算思维真题及详细解析第5套

少儿编程 蓝桥杯青少组计算思维真题及详细解析第5套 1、北京冬奥会经历 17( ),中国体育代表团收获的金牌数和奖牌数均创历史新高 A、年 B、月 C、天 D、小时 答案:C 考点分析:主要考查小朋友们对时事的了解,北京冬奥会总共经历了17天,所以答案C 2、下面图形的周长是…

Python系列模块之标准库json详解

感谢点赞和关注 &#xff0c;每天进步一点点&#xff01;加油&#xff01; 目录 一、Json介绍 二、JSON 函数 2.1 json.dumps 2.2 json.loads 2.3 实战案例&#xff1a;钉钉消息发送 一、Json介绍 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它使得人们…

2023年21个最佳的Ruby测试框架

作者 | Veethee Dixit 测试人员总是在寻找最好的自动化测试框架&#xff0c;它能提供丰富的功能&#xff0c;并且语法简单、兼容性好、执行速度快。如果你选择将Ruby与Selenium结合起来进行web测试&#xff0c;那么可能需要搜索基于Ruby的测试框架进行web应用程序测试。 Ruby…

【Python】函数式编程第二弹

知识目录 一、写在前面✨二、最小公倍数三、移除数字四、总结撒花&#x1f60a; 一、写在前面✨ 大家好&#xff01;我是初心&#xff0c;希望我们一路走来能坚守初心&#xff01; 今天跟大家分享的文章是 Python函数式编程第二弹&#xff0c;再次以两个简单的例子带大家更好…

selenium UI自动化中文件上传的两种方式

前言 文件上传是自动化中很常见的一个功能&#xff0c;那么对于文件上传你又有多少了解呢&#xff1f;请往下看 1、被测产品中文件上传的功能非常普遍&#xff0c;一般情况下需要将准备好的文件放在预定的路径下&#xff0c;然后在自动化测试的脚本中&#xff0c;去预置的路径…

国内可以免费使用的GPT

一、wetab新标签页 教程&#xff1a;https://diwlwltzssn.feishu.cn/docx/MnHhdvxATomBnMxfas2cm8wWnVd 装GPT界面&#xff1a;https://microsoftedge.microsoft.com/addons/detail/wetab%E5%85%8D%E8%B4%B9chatgpt%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5/bpelnogcookhocnaokfp…

TeX Live和TeX studio安装

最近想要研究一下Letex怎么写论文&#xff0c;然后就查阅资料了解了一下&#xff0c;先安装上两个软件&#xff0c;怎么用在研究研究&#xff0c;这里记录一下软件安装过程&#xff0c;方便以后查阅。 TeX Live和TeX studio安装 Latex介绍TexLive安装下载TexLive的安装包安装Te…

C++知识第三篇之继承

C继承 继承是面向对象编程的重要特征&#xff0c;是对类设计层次的复用 文章目录 C继承一.介绍1.继承定义2.继承方式3.class与struct 二.作用域1.成员变量2.成员函数 三.赋值转换1.给基类对象赋值2.给基类对象指针赋值 四.派生类的默认函数五. 其他1.友元2.静态 六.继承1.单继承…

Android车载学习笔记1——车载整体系统简介

一、汽车操作系统 汽车操作系统包括安全车载操作系统、智能驾驶操作系统和智能座舱操作系统。 1. 安全车载操作系统 安全车载操作系统主要面向经典车辆控制领域&#xff0c;如动力系统、底盘系统和车身系统等&#xff0c;该类操作系统对实时性和安全性要求极高&#xff0c;生态…

VCSA 和ESXi 6.7.0版本升级

1. VCSA升级步骤 1&#xff09;指定升级包的位置 software-packages stage --iso (如果是从vmware下载补丁&#xff0c;使用CD/DVD来映射ISO映像) 或 software-packages stage --url https://vapp-updates.vmware.com/vai-catalog/valm/vmw/8d167796-34d5-4899-be0a-6daade400…

Yolov8涨点神器:注意力机制---多头上下文集成(Context Aggregation)的广义构建模块,助力小目标检测,暴力涨点

🏆🏆🏆🏆🏆🏆Yolov8魔术师🏆🏆🏆🏆🏆🏆 ✨✨✨魔改网络、复现前沿论文,组合优化创新 🚀🚀🚀小目标、遮挡物、难样本性能提升 🍉🍉🍉定期更新不同数据集涨点情况 2.Context Aggregation介绍 论文:https://arxiv.org/abs/2106.01401 仅…

落地页设计的营销心理学(二)

作为一个营销落地页的设计者&#xff0c;最关注的问题&#xff1a;怎么样设计才能让我的落地页有转化&#xff1f; 当然这可能有很多影响因素。但是&#xff0c;你有没有想过在用户打开落地页时&#xff0c;运用心理学概念留住他们&#xff0c;促使他们完成留资、下载、购买等动…