当前位置: 首页 > 后端技术 > Java

Redis并发阻塞锁解决方案

时间:2023-04-01 14:30:50 Java

由于用户同时访问在线下单界面,在扣库存时出现异常。这是一个非常典型的并发问题。这篇文章就是为了解决并发问题而诞生的。该技术为Redis锁机制+多线程阻塞唤醒方式。在实现Redis锁机制之前,我们需要先了解一下前置知识。一、前置知识1、多线程将wait()和notifyAll()归为多线程方法略显不妥。这两个方法是Object中的方法。①wait()方法被调用时,让当前线程进入等待状态,同时让当前线程释放对象锁,等待阻塞状态,等待notifyAll()方法唤醒。wait()方法和sleep()方法有一些相似之处,都是阻塞当前线程,但实际上也有一些区别。在执行wait()方法之前,您需要请求一个锁。当wait()方法执行时,会释放锁,等待被唤醒时会竞争锁。sleep()只是让当前线程休眠一段时间,忽略锁的存在。wait()是Object类的一个方法sleep()是Thread的一个静态方法②notifyAll()方法是在wait()中唤醒线程。notifyAll()和notify()方法都可以在调用wait()方法后唤醒被阻塞的线程。但是notify()是随机唤醒阻塞队列中的一个随机线程,而notifyAll()是唤醒已经调用wait()方法而阻塞的线程,让它们自己抢占对象锁。notifyAll()和notify()也必须在锁定的同步代码块中调用。它们起到唤醒的作用,并不是释放锁。它们仅用于当前同步代码块中程序的执行。只有对象锁自然释放,notifyAll()和notify()方法才会起作用,唤醒线程。wait()方法通常与notify()或notifyAll()方法一起使用。以上就是掌握本博客所必需的多线程知识。如果系统学习了多线程的相关知识,可以参考田程序员的博客2。已经有价值了,只能放弃,以后再尝试。Redis提供了一种自然的方式来实现锁机制。Redis客户端的命令是setnx(setifnotexists)Springboot集成采用的方法是:redisTemplate.opsForValue().setIfAbsent(key,value);如果设置值成功则返回True,如果已经有值则返回False。当我们实际使用它的时候,setIfAbsent()方法并不总是返回True和False。如果我们的业务增加了交易,这个方法会返回null。我不知道这是一个错误还是什么。这是Redis的一个巨大的坑。找了很久才发现这个问题。如果你解决了这个问题,你可以跳到第四章。2.实现原理分布式锁的本质目标是在Redis中占有一席之地。当其他进程想要占用它时,发现已经有人占用了,只好放弃或者稍后再试。占用一般使用setnx(setifnotexists)命令,只允许一个client占用。先来先占,干完活再调用del命令释放厕所。其中,发现Redis中已经有值,当前线程是直接放弃还是稍后重试,分别代表非阻塞锁和阻塞锁。在我们的业务场景中,一定要稍后重试(阻塞锁)。如果我们直接放弃(非阻塞锁),我们可以直接在数据库层面做,不需要在代码上花很多时间。非阻塞锁只能保存数据的正确性,在高并发的情况下会抛出大量异常。当一百个并发请求到来时,只有一个请求成功,其他的都会抛出异常。Redis非阻塞锁和MySQL乐观锁的最终效果是一样的。乐观锁采用了CAS的思想。乐观锁法:在表字段中加一个版本号,或者其他字段也可以!加上版本号,就可以知道控制顺序了!更新的时候可以在where后面加上version=oldVersion。数据库,在任何并发情况下,更新成功为1,失败为0。可以根据返回的1、0做相应的处理!我们建议您使用阻塞锁。当无法获得锁时,我们使用wait()方法让当前线程唤醒,当持有锁的线程使用完后,调用notifyAll()唤醒所有等待的方法。三、具体实现下面的代码是阻塞锁的实现。业务层:publicStringtest()throwsInterruptedException{lock("lockKey");System.out.println("11");System.out.println("22");System.out.println(Thread.currentThread().getName()+"************");线程.睡眠(2000);System.out.println("33");System.out.println("44");System.out.println("55");解锁(“锁匙”);返回“字符串”;}锁工具类:主要是加锁和解锁两种方法。//每个rediskey对应一个阻塞对象privatestaticHashMapblockers=newHashMap<>();//当前获取锁的线程privatestaticThreadcurThread;公共静态RedisTemplateredisTemplate=(RedisTemplate)SpringUtils。getBean("redisTemplate");/***lock*@paramkey*@throwsInterruptedException*/publicstaticvoidlock(Stringkey){//循环判断是否可以创建key,如果不能,直接等待释放CPU执行权//可以'放进去,说明锁正在被占用System.out.println(key+"**");while(!RedisUtil.setLock(key,"1",3)){synchronized(key){blockers.put(key,key);//等待释放CPU执行权try{key.wait();}catch(InterruptedExceptione){e.printStackTrace();}}}blockers.put(key,key);//可以创建成功,获取锁成功记录当前获取锁线程curThread=Thread.currentThread();}/***开锁*@paramkey*/publicstaticvoidunlock(Stringkey){//判断是否解锁锁定的线程,如果不解锁则忽略if(curThread==Thread.currentThread()){RedisUtil.delete(key);//删除key后需要通知所有的应用,所以这里我们使用订阅消息给所有的应用//RedisUtil.publish("lock",key);//通知所有其他线程Objectlock=blockers.get(key);如果(锁!=null){同步(锁){lock.notifyAll();}}}}当我们使用接口测试工具进行无锁测试时,12345无法顺序执行,会导致输出顺序不一致,如果是在我们实际场景中,输入替换为数据库的select和update,数据乱序是正常的。我们加锁后,顺序输出12345,并发问题就顺利解决了。.4.附录一.Redis中的Bug本来lock()方法是直接调用“Redis.setIfAbsent()”方法,但是使用的时候总是报空指针异常。问题最终定位为Redis.setIfAbsent()方法的问题。在我的实际业务中,下单方法使用@Transflastion添加交易,导致方法返回null。我们编写一个函数来手动实现setIfAbsent()。/***只有当key不存在时才设置value,返回true,否则返回false**@paramkeykey不能为null*@paramvaluevalue不能为null*@paramtimeout过期时间,单位很妙*@return*/publicstaticBooleansetLock(Stringkey,Stringvalue,longtimeout){SessionCallbacksessionCallback=newSessionCallback(){Listexec=null;@Override@SuppressWarnings("unchecked")publicBooleanexecute(RedisOperationsoperations)throwsDataAccessException{operations.multi();redisTemplate.opsForValue().setIfAbsent(key,value);redisTemplate.expire(key,timeout,TimeUnit.SECONDS);exec=operations.exec();if(exec.size()>0){return(Boolean)exec.get(0);}返回假;}};返回(布尔值)redisTemplate.execute(sessionCallback);}为了对比,下面粘贴了原来的setIfAbsent()方法/***仅当key不存在时才设置值,返回true,否则返回false【警告:在事务或管道的情况下会报错——可以使用setLock方法】**@paramkeykey不能为null*@paramvaluevalue不能为null*@paramtimeout过期时间,单位很妙*@return*/@DeprecatedpublicstaticBooleansetIfAbsent(Stringkey,Tvalue,longtimeout){//redisTemplate.multi();ValueOperationsvalueOperations=redisTemplate.opsForValue();BooleanaBoolean=valueOperations.setIfAbsent(key,value,timeout,TimeUnit.SECONDS);//redisTemplate.exec();返回一个布尔值;}2.MySQL在并发场景下的锁机制MySQL会报错,报错信息如下:###Cause:com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:Lockwaittimeoutexceeded;尝试重启交易;SQL[];超过锁定等待超时;尝试重启交易;嵌套异常是com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:Lockwaittimeoutexceeded;tryrestarttransaction出现问题的原因是某类表经常被锁住,导致另一个事务超时。问题的原因是MySQL机制。MySQL更新时,如果where字段有索引,就会使用行锁,否则使用表锁。我们使用navichat在where字段上添加索引,问题顺利解决。