网站空间买多大的,公司注册地址规定,响应式网站如何做的,邢台做网站哪儿好前言#xff1a; 在Java后端业务中#xff0c; 如果我们开启了均衡负载模式#xff0c;也就是多台服务器处理前端的请求#xff0c;就会产生一个问题#xff1a;多台服务器就会有多个JVM#xff0c;多个JVM就会导致服务器集群下的并发问题。我们在这里提出的解决思路是把…前言 在Java后端业务中 如果我们开启了均衡负载模式也就是多台服务器处理前端的请求就会产生一个问题多台服务器就会有多个JVM多个JVM就会导致服务器集群下的并发问题。我们在这里提出的解决思路是把锁交给Redis来实现因为Redis是单线程的。而最基础的Redis解决集群模式下的并发问题的核心解决方案是使用Setnx构造分布式锁下文来让我们详细的看一下过程。 目录
前言
核心思路
具体业务逻辑
业务问题解决思路
1.选择加锁问题
2.Redis分布式锁的误删问题
3如何保证删除锁代码的原子性
业务杂项知识点
1.Spring mvc中的事务失效引起的并发问题
2.包装类与基本数据类型的差异
总结 核心思路 其实整个爆改过程的思路都很清楚我们先来解释一下SETNX的作用 SETNX key value SETNX命令的作用是只有当指定的键名 key 不存在时将键值对存储到Redis数据库中。如果键名 key 已经存在则不执行任何操作。 那么整体的核心思路就是让当前线程尝试先创建A再执行业务逻辑代码如果A不存在就进行创建并执行相关业务逻辑业务逻辑执行完毕后释放A如果A存在那么说明此时有其他的线程在执行业务逻辑代码则拒绝当前线程执行业务逻辑挂起线程
其实就是通过SETNX构造了一个唯一数据并且把这个数据作为锁。这种思路使得我们的锁不再局限于某一个JAVA对象从而避开了synchronized只能在JVM内部生效。解决了集群架构下多JVM上锁困难的困境 具体业务逻辑
本次的具体业务应用场景是优惠卷秒杀场景简单的来讲就是商家发放优惠卷用户进行抢购。而在优惠卷秒杀业务中我们需要注意的是一人一单问题。一人一单就是一个用户只允许下一单。而我们本项目的背景是允许多端登录。我们可以想一想这个问题的核心问题如果多端登录在服务器集群架构的模式下如果我们还是传统模式加锁就会出现这个问题 用户A同时登录的电脑和手机在以前的模式下我们是简单粗暴的给一人一单核心代码直接解锁。但这样做有两个问题 1.如果直接加锁那么也就是说程序的并发性大大降低我们一次只能处理一个用户的优惠卷订单效率大大降低。 2.如果是在集群模式下传统的锁只能在一个JVM内生效并不能跨JVM。如果用户的电脑购买优惠卷请求进入到了服务器A而用户的手机购买优惠卷请求进入到了服务器B那么就有可能造成优惠卷超卖的情况。 总结一下优惠卷超卖场景的业务逻辑
查询优惠卷是否存在查询优惠卷是否在售卖时间查询当前优惠卷是否还有库存查询用户是否已经下过单如果有直接返回给前端Result封装消息类扣减优惠卷库存创建订单ID返回订单号给前端封装订单相关信息更新数据库
在这几步中从4-8步就是一人一单问题而解决优惠卷秒杀问题大部分情况就是在解决这个问题。
业务问题解决思路
我们来一步一步看当前有哪些问题需要我们解决
1.选择加锁问题
在我们最开始的加锁中我们选择的是synchronized关键字但是它会导致程序的并发性大大降低。并且无法跨JVM容器生效。
我们为了解决synchronized关键字无法跨JVM容器生效采用了SETNX关键字。通过这种方法我们解决了锁跨JVM容器生效。
synchronized 是基于JVM层面的同步机制它会锁定整个方法而且它的作用范围限定在单个JVM内。在分布式系统或者集群环境中synchronized 不能跨JVM工作因此不适合作为分布式锁使用。而分布式锁 simpleRedisLock 是基于Redis实现的可以跨多个应用实例工作适用于分布式系统。 但是它本质上和synchronized关键字的作用一样并没有解决程序的并发性大大降低的问题。只不过以前我们是通过synchronized关键字拦截线程现在是通过SETNX拦截线程。 那么让我们来逆推一下思路加锁是为了解决两个问题
同一用户在不同端多次购买的相同优惠卷的行为不同用户同时购买同一优惠卷的行为。
而我们可以先来优化一下同一用户在不同端多购买的行为。按照我们之前的思路是不管三七二十一就上锁。如图所示可以理解为
但是我们真的有这个必要嘛我们仔细想一想如果只是为了避免同一用户在不同端多次购买的相同优惠卷那么我们只需要针对这个用户加锁不就好了嘛 也就是说现在我们设计的锁应该是只会拦截同一个用户的多次登录而不拦截多个用户的并发登录。如图所示可以理解为 我们从代码层面解释一下我们利用SETNX创建key的时候将key设置为USERID。那么此时就会出现两种情况
1.同一用户多端登录发送购票请求由于SETNX创建KEY的时候是根据UserID创建的因此只能有一个端创建key成功实现了为同一用户加锁避免多端登录购票。
2.不同的用户由于UserID不同因此SETNX创建KEY的时候不会失败也就是说不会被拦截。
也就是说我们通过根据UserID构造key的方式实现了为每个用户加锁提高了程序的并发性能。
我们再来解决一下多个用户同时购买同一优惠卷的问题。我们再来转变一下角度之所以要处理多个用户同时购买同一优惠卷是因为会存在超卖问题。而我们如何除了加锁之外还有没有其他的方法解决超卖问题呢 答案是有的.我们在每一次扣减库存的时候都同步判断一下当前数据库中优惠卷库存是否大于0不就好了嘛 当然这里要保证判断库存和扣减库存的原子性不可以被打断。 其实这里的思路就是CAS算法即Compare And Swap 那么选择加锁问题我们已经解决了为了优化普通模式下加锁的无法跨JVM容器和拷打并发性的问题我们采用了以下两个步骤
无法跨容器使用Redis中的SETNX来保证锁可跨JVM容器并发性差利用userID构造每个用户专属的锁并且通过数据库操作维护多用户下单超卖问题。
此时我们用流程图来展示一下当前的执行逻辑 当然了为了避免死锁的出现我们要为SETNX构造出的键值对设置过期时间防止死锁的出现。
而接下来的问题也就是我们要着重介绍的一个问题
2.Redis分布式锁的误删问题 此处我们说的是同一用户多端登录引发的并发性问题而不同用户之间由于构造的时候key就不一样因此不存在误删问题。 在我们前面构造的业务逻辑中理想的状态应该是 在理想状态下多段登录可以正确的创建和释放锁维护程序的并发性而在我们的业务逻辑中可能会出现如下异常情况
这段异常简单的来讲线程1的阻塞使得线程1所创建的用户锁被超时释放此时Redis中并没有针对当前用户的锁当前用户再发起一个线程2线程2获取到锁。而线程1此时阻塞结束开始执行业务和最后删除锁的操作导致线程2创建的当前用户锁被删除。此时线程2在执行自己的业务但是整个redis中已经无针对当前用户的锁了。线程3此时尝试获取锁获取成功。那么在这种环境下线程123都获取到了锁并且执行了买票业务。
这种业务场景虽然少见但仍是我们要解决的问题。 而解决的思路也很简单主要的思路设置锁标识让每个线程只能删除自己的锁 也就是说以前我们利用SETNX创建锁的时候是不管锁的value值的现在为了解决锁的误删问题我们要给value中赋值使其成为锁标识。
我们看看代码
创建锁
删除锁 但是这样就对了嘛
其实是不对的 这是因为我们在unlock里面执行了多条语句可能在获取锁的标识的时候还没来得及执行delete语句线程就又被阻塞了此时就又会发生我们之前说的误删问题。
3如何保证删除锁代码的原子性
在这里我们使用的是lua脚本。Redis提供了lua脚本功能在一个脚本中编写多条Redis命令确保多条命令执行时的原子性。 关于lua脚本的书写我们这里不做具体介绍感兴趣的同学可以自学lua是基于c语言实现的他的语法结构很简单。 Lua 教程 (w3schools.cn)https://www.w3schools.cn/lua/index.asp 将之前的unlock中的redis操作转化为lua脚本然后再交给redis执行。
我们来看看代码
通过这种方式我们就确保了多条Redis命令的原子性解决了删除锁代码的原子性问题。
业务杂项知识点
1.Spring mvc中的事务失效引起的并发问题
在代码框架设计的时候我把4-8过程单独拉出来封装了一个方法 封装部分代码 为了保证扣减库存的时候执行的多条SQL语句的原子性我们加上了Transactional注解。然后在获取锁后执行业务逻辑代码的时候调用这个方法。
但这也就是一个坑点Spring mvc中的事务是会失效的。 在Spring框架中声明式事务管理依赖于AOP面向切面编程。当我们在一个方法上使用Transactional注解时Spring将创建一个代理对象来包装原始的Bean。这个代理对象会在方法调用前后添加事务管理的逻辑如开启和关闭事务以及在发生异常时进行回滚操作。 如果直接调用同一个类中的另一个Transactional方法由于是内部调用并不会经过代理对象因此事务管理相关的逻辑不会被执行。这就是为什么通常建议将事务管理放在服务层Service Layer并且只通过注入的方式跨类调用事务方法确保每次调用都能通过代理对象从而让AOP能够正确地应用事务管理的逻辑。 如果不使用Spring AOP代理机制那么Transactional注解将不会生效因为没有任何机制来拦截方法调用并应用事务的边界。这意味着即使定义了事务也不会有实际的事务行为发生如开始新事务、加入现有事务或在发生异常时回滚事务。 总结来说Spring的声明式事务管理是通过AOP代理实现的不使用AOP代理将导致事务失效。要确保事务能够正常工作必须遵循Spring的配置和使用准则确保通过代理对象对事务方法进行调用。 因此在调用这个方法时候我们不能直接调用这种方式是错误的 而应该这么调用 2.包装类与基本数据类型的差异
当我们使用stringRedisTemplate来操作Redis的时候返回值会有包装类型例如Boolean。 但是如果我们直接这样返回的话会出现一个问题我们要求的返回值类型是boolean也就是基本数据类型。虽然Boolean会有自动拆箱功能可以自动转换为boolean但是可能会出现空指针异常
这是为什么呢原因很简单Boolean是包装类可以存放空值而在自动拆箱的时候空值会转变为空指针。而基本数据类型不允许存储空指针。因此直接抛出空指针异常。
总结 经过本文的讲解我们了解了如何利用Redis实现一个简单的分布式锁。而其实Redis就已经为我们提供了一套高性能高可用的分布式锁Redission。在之后的文章我也会给大家介绍如何使用Redission。
如果我的内容对你有帮助请点赞评论收藏。创作不易大家的支持就是我坚持下去的动力