单机体系的锁
源码在这
git clone https://gitee.com/nineidei/redisDeom.git
使用ReentrantLock作为锁
两个模块中的代码一致
nginx的配置
在我源码中可能端口号发生变化,所以请大家按照实际情况来处理
图1
使用Jmeter模拟高并发,通过nginx转发到两个模块的接口,最后查看redis中设置的商品库存量和控制台打印的日志
能明显看到,商品在我100个并发进来之后,明明应该是0的,此时还剩12个
观察两个模块的打印信息,发现重复消费了,我们加的lock锁没有生效,正如图1中lock锁只能针对自己的JVM上锁,如果是另一个JVM则无法管理
分布式体系下的锁
第一版
原理
在redis中使用set , get 的方式来加锁,set RedisLock xxx 之后,如果再次get RedisLock就会查到xxx的信息,等拿到redis锁的线程执行完流程之后就delete RedisLock解锁,递归的线程会再次尝试set RedisLock xxx
如图
代码如下
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果flag为false,就代表加锁失败,需要重新请求
if (!flag){
//为了减轻服务器压力,延迟20ms
try {
TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
//然后重新请求
sale();
}else {
//抢到了就正常的流程
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
redisTemplate.delete(key);
}
}
return retMessage;
}
在大量并发下,也仅仅只能在单机模式下使用,如果是分布式下依旧会出现超卖现象
问题:
在高并发环境下,是严禁使用递归的,因为容易造成堆栈溢出
解决:
使用whlie代替if,使用自旋的方式代替递归重试
第二版
使用whlie代替if,使用自旋的方式代替递归重试
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
//使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
while(!redisTemplate.opsForValue().setIfAbsent(key, value)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
//如果抢到锁,就走正常逻辑
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
redisTemplate.delete(key);
}
return retMessage;
}
原理
将判断直接放入while中,这样做就可以重复多次去访问redis,而不需要通过方法调方法来判断能否抢到锁,且不会造成堆栈溢出的问题
问题:
如果在while代码中发生了异常,走不到finally中,岂不是永远都无法解锁了?如果其他用户访问,将会一直陷入while循环中
解决:
每次加锁的时候赋予上一个过期时间,即便是报错了,时间一到也会自动解锁
第三版
原理
每次加锁的时候赋予上一个过期时间
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
//使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
while(!redisTemplate.opsForValue().setIfAbsent(key, value)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
//如果建锁成功,马上加上过期时间
redisTemplate.expire(key,30L,TimeUnit.SECONDS);
//如果抢到锁,就走正常逻辑
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
redisTemplate.delete(key);
}
return retMessage;
}
问题:
加锁和赋予key的过期时间不是原子性的,如果在高并发下加锁后立马报错,没有走到赋予时间这个方法,依旧会导致第二版的问题
解决:
将两行代码合并成一行,形成原子性操作
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
//加锁和赋予过期时间必须是同一行,保证原子性
while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
//如果建锁成功,马上加上过期时间
//redisTemplate.expire(key,30L,TimeUnit.SECONDS);
//如果抢到锁,就走正常逻辑
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
redisTemplate.delete(key);
}
return retMessage;
}
第四版
原理
将两行代码合并成一行,形成原子性操作
问题:
实际业务处理时间如果超过了默认设置的过期时间,会删除其他人的锁
解决:
只允许自己删除自己的锁,不允许删除其他人的
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
//使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
//如果建锁成功,马上加上过期时间
//redisTemplate.expire(key,30L,TimeUnit.SECONDS);
//如果抢到锁,就走正常逻辑
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//改进点,只能删除自己的key,不能删除他人的
if (redisTemplate.opsForValue().get(key).toString().equalsIgnoreCase(value.toString())){
redisTemplate.delete(key);
}
}
return retMessage;
}
第五版
原理
在删除自己key的同时做一个判断,查看是否是自己的key,否则不能删除
问题:
finally代码块中的del方法不是原子性的
但是,如果代码走到if中,去查询这个key突然发生问题,停在了if,这个时候无法走到delet删除这个key,也就是说必须是原子性的操作,在if中查的同时,如果判断为true就立马删除
解决:
使用lua脚本作为502粘合剂,将if中的判断和del粘合起来~
这里简单使用lua脚本进行几个案例的实操
这是最简单的lua脚本入门
EVAL命令就是调一段脚本,“return”就表示我需要返回的什么东西,我这里就是'hello lua'
如果我想把这三个指令写成一个应该如何写呢?使用lua脚本实现
redis.call() : 只要我们想通过lua脚本调用redis中的命令,就需要使用这种方式
那么该如何使用动态传参的方式改写这个lua脚本呢?
不知道各位有没有看到后面numkeys的传参,如果我在后面加上0,就表示什么都不用传
如果是2,就表示有两个key和两个val,通过KEYS[1],和ARGV[1]获取我们需要传入的参数
再进一步学习Lua脚本
这次加上if和else判断,这是官网的例子,我们改造一下
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 redisLock 111112222
进行get(key)的判断,如果等于我传入的value值那么就删除,否则删不了
那么根据Lua脚本,我们将finally代码块中的查询以及删除操作整合为原子性操作
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Object key = "RedisLock";
Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
//如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
//使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
//如果建锁成功,马上加上过期时间
//redisTemplate.expire(key,30L,TimeUnit.SECONDS);
//如果抢到锁,就走正常逻辑
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//改进点,修改Lua脚本的redis分布式锁调用,必须保证原子性
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del' , KEYS[1])" +
"else " +
"return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),value);
}
return retMessage;
}
代码写到这里,对于一些小的自研已经是够用了,但是针对大厂还是有不足的地方
还需要设计成可重入锁以及增加锁的自动续期功能
第六版:
如何写好一个锁
一个靠谱的分布式锁需要具备:
独占性
高可用
防死锁:必须要加过期时间,如果java宕机了,redis那边到期自动消失
不乱抢:使用Lua脚本,查询redis中自己占有的锁,然后删除这一系列操作的原子性
重入性:只要是自己持有自己的锁,再进去的话,就不用重新申请一把锁,相当于一把锁能开所有自己权限内所有的门,而不是每到一个门就要用一个新的锁,从而造成锁套锁
可以想象成回家之后进大门需要一把锁,只要我进入了大门,那么其他房间的门我就都能进了,而不是说卧室也要像大门一样再新建一个防盗门,这就是可重入性
一句话:同一个线程中多个流程可以获得同一把锁,持有这把同步锁的线程能再次进入。
目前有两个分支,目的是保证同一时期,只能有一个线程持有锁进入redis做库存扣减的动作
一 保证加锁解锁,lock/unlock
二 扣减库存redis命令的原子性
第一步:先判断锁是否存在 (EXISTS key)
第二步:不存在就说明hset新建当前线程属于自己的锁BY UUID:ThreadID
HSET RedisLock 5hk26g3jh7fgh36f6g26gk2l:1 1
RedisLock为key
5hk26g3jh7fgh36f6g26gk2l:1 为value
1为可重入次数
如果返回 1 就说明已经有锁,需要进一步判断是不是当前线程自己的
EXISTS RedisLock 5hk26g3jh7fgh36f6g26gk2l:1
返回0表示不是自己的
返回1说明是自己的锁,自增1次表示重入
HINCRBY RedisLock 5hk26g3jh7fgh36f6g26gk2l:1 1
//v1
//加锁脚本
if redis.call('exists','key') == 0 then
redis.call('hset','key','uuid:threadID') == 1
redis.call('expire','key',50)
return 1
elseif redis.call('hexists','key','uuid:threadID') == 1 then
redis.call('hincrby','key','uuid:threadID',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//先查key值,如果为0就表示当前没有锁,就根据当前线程创建一个锁,并且将锁的存活时间这是为50s
//如果存在就查询这个key值以及当前线程是否返回1,如果返回1就证明是当前线程持有的锁,在1的基础上加1,表示重入一次,然后将锁的过期时间设置为50s
//如果到最后的else中,就表示不是自己的锁,返回0,接着走while循环抢锁
还可以优化一下
//使用hincrby代替hset
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadID') == 1 then
redis.call('hincrby','key','uuid:threadID',1)
redis.call('expire','key',50)
return 1
else
return 0
end
再优化一下,将参数换上去
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadID') == 1 then
redis.call('hincrby','key','uuid:threadID',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//key -->> KEYS[1] -->> RedisLock
//value -->> ARGV[1] -->> 4j32lj42hghjgbs45sf6:1
//过期时间值 -->> ARGV[2] -->> 30 s
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
然后放到redis中测试下
同时也满足了可重入的要求
解锁Lua脚本
if redis.call('hexists',key,uuid:threadId) == 0 then
return nil
elseif redis.call('hincrby',key,uuid:threadId,-1) == 0 then
return redis.call('del',key)
else
return 0
end
优化一下
if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then
return redis.call('del',KEYS[1])
else
return 0
end
java微服务整合lua脚本加锁解锁
@RestController
public class HelloController {
@Resource
private StringRedisTemplate redisTemplate;
Lock myRedisLock = new RedisDistributedLock(redisTemplate,"RedisLock");
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
myRedisLock.lock();
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
myRedisLock.unlock();
}
return retMessage;
}
}
//自研的redis分布式锁
public class RedisDistributedLock implements Lock {
@Autowired
private StringRedisTemplate redisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private Long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuidValue = UUID.randomUUID() + ":" +Thread.currentThread().getId();
this.expireTime = 50L;
}
@Override
public void lock() {
tryLock();
}
@Override
public void unlock() {
String strict =
"if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else return 0 " +
"end";
Long flag = redisTemplate.execute(new DefaultRedisScript<>(strict, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if (null == flag){
throw new RuntimeException("this lock doesn't exists!!!");
}
}
@Override
public boolean tryLock() {
try {
tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L){
String strict =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
while (!redisTemplate.execute(new DefaultRedisScript<>(strict,Boolean.class),Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
//如果加锁失败,等待60s再次加锁
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
return true;
}
return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
通过我自己创建的RedisDistributedLock实现Lock接口,重写lock,unlock,tryLock方法,使用Lua脚本的方式实现原子性操作,并且保证可重入性,以及以自旋的方式加锁,但是依旧有缺陷,因为我们把redis获得锁写死了,假如以后有zookeeper,mysql做分布式锁,还需要再加入工厂模式。
为什么使用工厂模式?
1. 如果我想要一条狗,我是不是得自己new一个?
Dog dog = new dog();
这样不就写死了吗,没有任何通用性,只能得到一条狗
2. 利用多态
创造一个Animal a = new 鸡(); 鸭(); 鱼(); 狗();
左边是接口,右边是具体的实体类,经典的就是
List list = new ArrayList();
List list = new LinkList();等等
3. 我们希望左边是固定的,右边是动态的。多态+动态,提交给spring容器管理,或者池化技术
4. 设计模式,通过工厂模式,直接通过传参从工厂获得
5. 设计一个分布式锁的工厂
@Component //纳入spring容器管理
public class RedisDistributedLockFactory {
private String lockName;
@Autowired
private StringRedisTemplate redisTemplate;
public Lock getDistributedLock(String lockType){
if (lockType== null){
return null;
}
if (lockType.equalsIgnoreCase("REDIS")){
this.lockName = "RedisLock";
return new RedisDistributedLock(redisTemplate,lockName);
}
if (lockType.equalsIgnoreCase("ZOOKEEPER")){
this.lockName = "ZookeeperLockNode";
//TODO
}
if (lockType.equalsIgnoreCase("MYSQL")){
this.lockName = "MysqlLock";
//TODO
}
return null;
}
}
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
//这里使用工厂拿到redis分布式锁
Lock myRedisLock = lockFactory.getDistributedLock("REDIS");
myRedisLock.lock();
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
myRedisLock.unlock();
}
return retMessage;
}
测试可重入性
@RequestMapping("/sale")
public String sale(){
String retMessage = "";
Lock myRedisLock = lockFactory.getDistributedLock("REDIS");
myRedisLock.lock();
try{
testReEntry();
}finally {
myRedisLock.unlock();
}
return retMessage;
}
private void testReEntry(){
Lock redisLock = lockFactory.getDistributedLock("REDIS");
redisLock.lock();
try {
System.out.println("=========测试可重入锁============");
}finally {
redisLock.unlock();
}
}
出现了bug
这是因为工厂模式导致的,每一次都是new过来的,每new一次,线程id就变一次
@Component //纳入spring容器管理
public class RedisDistributedLockFactory {
private String lockName;
private String uuid;
public RedisDistributedLockFactory() {
this.uuid = UUID.randomUUID().toString();
}
@Autowired
private StringRedisTemplate redisTemplate;
public Lock getDistributedLock(String lockType){
if (lockType== null){
return null;
}
if (lockType.equalsIgnoreCase("REDIS")){
this.lockName = "RedisLock";
return new RedisDistributedLock(redisTemplate,lockName,uuid);
}
if (lockType.equalsIgnoreCase("ZOOKEEPER")){
this.lockName = "ZookeeperLockNode";
//TODO
}
if (lockType.equalsIgnoreCase("MYSQL")){
this.lockName = "MysqlLock";
//TODO
}
return null;
}
}
测试成功!!!
第七版
增加自动续期功能
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
将lua脚本放入代码中
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L){
String strict =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("加锁 - lockName :" + lockName +"\t" +"uuidValue" + uuidValue);
while (!redisTemplate.execute(new DefaultRedisScript<>(strict,Boolean.class),Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
//如果加锁失败,等待60s再次加锁
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
renewExpire();
}
return true;
}
return false;
}
private void renewExpire() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},((this.expireTime * 1000) /3));
}
总结一下
redis分布式锁我们需要考虑:
一. 宕机与过期
二. 防死锁
三. 防止误删key
四. Lua脚本保证原子性
五. 可重入锁(使用hset ,hash类型来处理)
六. 锁自动续期
lock() 加锁的关键逻辑
加锁:实际上就是在redis中,给Key设置一个值,为避免死锁,给一个过期时间
自旋
续期
unlock() 加锁的关键逻辑
考虑到可重入性的递减,加锁几次就要解锁几次
最后到0了,直接del
Redisson分布式锁的使用
RedisConfig配置类
@Configuration
public class RedisConfig {
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer()
// use "rediss://" for SSL connection
.setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
为了方便直接在控制层写出来
@Autowired
private Redisson redisson;
//v9.0 引入官网Redisson对应的官网推荐RedLock算法实现类
@RequestMapping("/saleNew")
public String saleNew(){
String retMessage = "";
RLock lock = redisson.getLock("RedisLock");
lock.lock();
try{
//1 查询库存余量
String result = redisTemplate.opsForValue().get("inventory001").toString();
//2判断库存是否足够
Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
//3 扣库存,每次减少一个
if (inventoryNum != 0){
redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
System.out.println(inventoryNum);
}else {
retMessage = "商品卖完了!";
}
}finally {
//改进点,只能删除属于自己的key,不能删除别人的
if (lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
return retMessage;
}
Redisson简单好用,更多关于Redisson的使用说明可以去官网
https://github.com/redisson/redisson#quick-start