synchronized一个jvm只有一个锁监视器    当tomact集群部署的话,锁监视器synchronized会失效,所以集群部署采用分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
(多进程可见,互斥,高可用,高性能,安全性)

基于redis的分布式锁

获取锁
互斥:确保只能有一个线程获取锁
释放锁:手动释放
             超时释放

方案一  我们需要在redis钟利用set key value  ex time  nx 建立分布式锁

    public boolean tryLock(long timeoutSec) {
        //获取线程的id
        String id =Thread.currentThread().getId();
        //获取锁
        Boolean success=  stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);//自动拆箱防止空指针
    }

如果redis中已经存在直接return false 这样多个集群只有这一个锁

然后释放锁

 stringRedisTemplate.delete(KEY_PREFIX + name);

但是如图所示

加入线程一获取锁但是业务阻塞,此时redis锁因为超时释放锁,正好线程二乘虚而入,获取到锁了,但是显线程一业务完成了,再次执行释放锁这个任务,此时释放的就是线程二的锁,所以我们要在原来的基础上进行value的判断,我们需要进行判断是否是自己的线程,如果是就删除,不是就不用管

   private static  final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
        String id =ID_PREFIX+Thread.currentThread().getId();

这样去定义value可以增强value的复杂度,避免两个jvm中线程id正好相等导致误删

在获取锁时存入线程标识(UUID) 一致释放,不一致不释放
采用的是非阻塞机制

这是最终的实现代码unlock中

    @Override
   public void unLock() {
        //获取线程标识
        String threadId=ID_PREFIX+Thread.currentThread().getId();        //获取锁中的标识
       String s= stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);        //判断表示是否一致
        //进行释放锁
        if(s.equals(threadId)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
   }

但是没有确保原子一致性,当线程一判断是属于自己的id后,此时jvm垃圾回收导致线程阻塞,此时线程二获取锁,业务没有执行完,线程一开始释放锁了,就会导致发生误删

我们可以采用lua脚本的形式进行删除,在确保valu相同的同时还进行释放锁

具体代码如下

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements  ILock{
    private StringRedisTemplate stringRedisTemplate;
    private static final DefaultRedisScript UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT =new DefaultRedisScript();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate,String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name=name;
    }
    private String name;
    private static  final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static  final  String KEY_PREFIX="lock:";
    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程的id
        String id =ID_PREFIX+Thread.currentThread().getId();
        //获取锁
        Boolean success=  stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);//自动拆箱防止空指针
    }

    @Override
    public void unLock() {
        //使用lua脚本进行删除
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());
    }

//    @Override
//    public void unLock() {
//        //获取线程标识
//        String threadId=ID_PREFIX+Thread.currentThread().getId();
//        //获取锁中的标识
//       String s= stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);
//        //判断表示是否一致
//        //进行释放锁
//        if(s.equals(threadId)) {
//            stringRedisTemplate.delete(KEY_PREFIX + name);
//        }
//    }
}
--对其进行比较
if(redis.call('get',KEYS[1]==ARGV[1]))then
	return redis.call('del',KEYS[1])
end
return 0

分布式锁的优化
使用redisson

代码中redis分布式锁中主要有几个不足的地方1,不可重入2,不可重试3,超时释放导致的安全问题4,主从一致性

可重试锁原理

然后构建redisson来进行trylock和unlock

package com.hmdp.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
       config.useSingleServer().setAddress("redis://.....:6379");
        //创建redissonClient对象
       return Redisson.create(config);
    }
}

然后直接用,因为redisson释放锁和获取底层都是用lua代码进行写的,所以也确保了redis和mysql原子一致性

       //创建锁对象
        //SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
      //  boolean isLock = simpleRedisLock.tryLock(1200);
        boolean isLock = lock.tryLock();
        //判断是否成功
        if(!isLock){
            //获取锁失败  返回错误   或者  进行重试
            return Result.fail("一个人不允许重复下单");
        }
try {
    //获取动态代理对象
    IVoucherOrderService o = (IVoucherOrderService) AopContext.currentProxy();
    //因为直接调用是this目标对象,而不是动态代理对象 我们需要得到动态代理对象 事务提交是由spring进行管理的
    return o.createVoucherOrder(voucherId);
}finally {
    //进行释放锁
 //   simpleRedisLock.unLock();
    lock.unlock();//看门狗机制 30s
}
Logo

更多推荐