1:使用 set key value ex nx 命令
nx:Redis 会先检查指定的 key 是否已经存在,如果不存在则使用 SETNX 命令为其设置新值。这个过程是原子性的,可以保证在并发环境下同一时刻只有一个客户端能够成功地为一个 key 设定值。
ex:可以为key设置一个过期时间,到了规定时间,这个锁就会自动失效
-
锁的重入问题:同一个线程多次获取锁的场景,目前不支持,可能会导致死锁
-
锁失败的重试问题:获取锁失败后要不要重试?目前是直接失败,不支持重试
-
Redis主从的一致性问题:由于主从同步存在延迟,当线程在主节点获取锁后,从节点可能未同步锁信息。如果此时主宕机,会出现锁失效情况。此时会有其它线程也获取锁成功。从而出现并发安全问题。
-
...
当然,上述问题并非无法解决,只不过会比较麻烦。例如:
-
原子性问题:可以利用Redis的LUA脚本来编写锁操作,确保原子性
-
超时问题:利用WatchDog(看门狗)机制,获取锁成功时开启一个定时任务,在锁到期前自动续期,避免超时释放。而当服务宕机后,WatchDog跟着停止运行,不会导致死锁。
-
锁重入问题:可以模拟Synchronized原理,放弃setnx,而是利用Redis的Hash结构来记录锁的持有者以及重入次数,获取锁时重入次数+1,释放锁是重入次数-1,次数为0则锁删除
-
主从一致性问题:可以利用Redis官网推荐的RedLock机制来解决
因此,我们只要会使用Redisson,即可解决上述问题,无需自己动手编码了
2 .redisson
1.引入redisson 依赖
2.配置连接redis
3.
利用Redisson获取锁时可以传3个参数:
-
waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
-
leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
-
TimeUnit:时间单位
Redisson的分布式锁使用并不复杂,基本步骤包括:
-
1)创建锁对象
-
2)尝试获取锁
-
3)处理业务
-
4)释放锁
但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强。
注解本身起到标记作用,同时还要带上锁参数:
-
锁名称
-
锁等待时间
-
锁超时时间
-
时间单位
锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock
注解的参数,交给用户去选择自己要用的类型。这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式。
1.我们首先定义一个锁类型枚举:
2.然后在自定义注解中添加锁类型这个参数:
3.然后定义一个锁工厂,用于根据锁类型创建锁对象:
4.我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:
此时,在业务中,就能通过注解来指定自己要用的锁类型了
多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。
大的方面来说,获取锁失败要从两方面来考虑:
-
获取锁失败是否要重试?有三种策略:
-
不重试,对应API:
lock.tryLock(0, 10, SECONDS)
,也就是waitTime小于等于0 -
有限次数重试:对应API:
lock.tryLock(5, 10, SECONDS)
,也就是waitTime大于0,重试一定waitTime时间后结束 -
无限重试:对应API
lock.lock(10, SECONDS)
, lock就是无限重试
-
-
重试失败后怎么处理?有两种策略:
-
直接结束
-
抛出异常
-
策略模式。同时,我们还需要定义一个失败策略的枚举。在MyLock注解中定义这个枚举类型的参数,供用户选择。然后,在MyLock注解中添加枚举参数:最后,修改切面代码,基于用户选择的策略来处理:这个时候,我们就可以在使用锁的时候自由选择锁类型、锁策略了: