文章目录

  • 前言:redisson是什么?
  • 一、几种操作Redis的Java Client
  • 二、redisson分布式锁
  • 1.引入背景
  • 原生分布式锁
  • 可重入锁和不可重入锁
  • 改为可重入锁
  • 加锁思路
  • 解锁
  • 最终代码
  • Redisson分布式锁
  • 引入依赖
  • 配置config
  • 启用分布式锁
  • 信号量(如果遇到需要其他进程也能解锁的情况,请使用分布式信号量.)
  • redis可重入锁(Reentrant Lock)
  • 读写锁
  • 闭锁
  • 总结

前言:redisson是什么?

Redisson is a Redis Java client(客户端) with features of In-Memory(内存) Data Grid. It provides more convenient and easiest way to work with Redis. Redisson objects provides a separation(分离) of concern, which allows you to keep focus on the data modeling and application logic.

一、几种操作Redis的Java Client

Redisson和Jedis、Lettuce
Redisson和它俩的区别就像一个用鼠标操作图形化界面,一个用命令行操作文件。Redisson是更高层的抽象,Jedis和Lettuce是Redis命令的封装。

  • Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持
  • Lettuce是一种可扩展的线程安全的 Redis 客户端,通讯框架基于Netty,支持高级的 Redis特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis成为首选 Redis 的客户端。
  • Redisson是架设在Redis基础上,通讯基于Netty的综合的、新型的中间件
  • 企业级开发中使用Redis的最佳范本Jedis把Redis命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者也都点到为止,只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集。

二、redisson分布式锁

1.引入背景

本地缓存问题:每个微服务都要有缓存服务、数据更新时只更新自己的缓存,造成缓存数据不一致

解决方案: 分布式缓存,微服务共用 缓存中间件

原生分布式锁

RedisTemplate来操作Redis
// 加锁
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {    
return redisTemplate.opsForValue()
				.setIfAbsent(key, value, timeout, unit);
}// 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName, String uuid) {    
	if(uuid.equals(redisTemplate.opsForValue().get(lockName)){
	 	redisTemplate.opsForValue().del(lockName);
	 }
}// 结构if(tryLock){    // todo}
finally{
    unlock;
}

这是锁没错,但get和del操作非原子性,并发一旦大了,无法保证进程安全,我们可以使用Lua脚本

lockDel.lua文件如下

if redis.call("get",KEYS[1]) == ARGV[1] 
then
	return redis.call("del",KEYS[1])
else
    return 0
end;
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("lockDel.lua")));
// 执行lua脚本解锁 , 后面两个参数是传入的值!!
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);

这样设计的话,我们发现是一个不可重入锁. synchronized和ReentrantLock都很丝滑,因为他们都是可重入锁,一个线程多次拿锁也不会死锁,我们需要可重入。

可重入锁和不可重入锁

可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得该锁。

这种情景,可以是不同的线程分别调用这个两个方法。也可是同一个线程,A方法中调用B方法,这个线程调用A方法。

不可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得不了该锁,必须等A方法释放掉这个锁。

改为可重入锁

重点:
同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点synchronized偏向锁提供了很好的思路,synchronized的实现重入是在JVM层面,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS

参数

示例值

含义

KEY个数

1

KEY个数

KEYS[1]

my_first_lock_name

锁名

ARGV[1]

60000

持有锁的有效时间:毫秒

ARGV[2]

58c62432-bb74-4d14-8a00-9908cc8b828f:1

唯一标识:获取锁时set

加锁思路


redis 缓存能永久保存数据吗 redisson 缓存_java

-- 存储结构 利用Redis的Hash来存储这些东西
--	lockname 锁名称
--   key1:   threadId   唯一键,线程id
--   value1:  count     计数器,记录该线程获取锁的次数

-- 计数器的加减  当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减
-- 判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists
-- redis也有hash自增的命令hincrby


-- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间 -- key1 为锁 arg1为过期时间  arg2为uuid
if (redis.call('exists', KEYS[1]) == 0)
then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
 
-- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
 
-- 若锁存在,但唯一标识不匹配:表明锁是被其他线程占用,当前线程无权解他人的锁,直接返回锁剩余过期时间
return redis.call('pttl', KEYS[1]);
解锁

redis 缓存能永久保存数据吗 redisson 缓存_redis_02

-- 若锁不存在:则直接广播解锁消息,并返回1
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1; 
end;
 
-- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end; 
 
-- 若锁存在,且唯一标识匹配:则先将锁重入计数减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then 
    -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return 0; 
else 
    -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程
    redis.call('del', KEYS[1]); 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1;
end;
 
return nil;
最终代码
/**
 * @description 原生redis实现分布式锁
 **/@Getter@Setterpublic 
 class RedisLock {    
 	private RedisTemplate redisTemplate;   
 	 
 	private DefaultRedisScript<Long> lockScript; 
 	  
  	private DefaultRedisScript<Object> unlockScript; 
  	  
    public RedisLock(RedisTemplate redisTemplate) {        
    	this.redisTemplate = redisTemplate; 
    	
   	 	lockScript = new DefaultRedisScript<>(); // 加载加锁的脚本
   	 	
    	this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));     
    	   
    	this.lockScript.setResultType(Long.class);        // 加载释放锁的脚本
    	
    	unlockScript = new DefaultRedisScript<>();     
    	   
    	this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
    }   
     /**
     * 获取锁
     */
    public String tryLock(String lockName, long releaseTime) {        // 存入的线程信息的前缀
        String key = UUID.randomUUID().toString();        // 执行脚本
        Long result = (Long) redisTemplate.execute(
                lockScript,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId(),
          		releaseTime);        
          		if (result != null && result.intValue() == 1) {
          			return key;
       			 } else {
       			 	return null;
       			 }
    }    /**
     * 解锁
     * @param lockName
     * @param key
     */
    public void unlock(String lockName, String key) {
        redisTemplate.execute(unlockScript,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId()
                );
    }
}
  • 加锁时判断nset
  • 使用时续期
  • 删除时判断uuid
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
    // 生成uuid
    String uuid = UUID.randomUUID().toString();
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    // 尝试加锁,阻塞0.5s
    Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
    // 加锁成功
    if (lock) {
        //  执行业务
        Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
        // 去删除锁
        String lockValue = ops.get("lock");
        // get和delete原子操作
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
            "    return redis.call(\"del\",KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        stringRedisTemplate.execute(
            new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
            Arrays.asList("lock"), // 参数key
            lockValue); // 参数值,锁的值  uuid
        // 返回db
        return categoriesDb;
    }else { // 加锁失败
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 睡眠0.1s后,重新调用 //自旋
        return getCatalogJsonDbWithRedisLock();
    }
}

Redisson分布式锁

引入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

配置config

@Configuration
public class MyRedissionConfig {
//    @Value("${ipAddr}")
//    private String ipAddr;

    // redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
    @Bean(destroyMethod = "shutdown")  //销毁时调用shutdown方法
    public RedissonClient redisson() {
        Config config = new Config();
        // 创建单例模式的配置
        config.useSingleServer().setAddress("redis://192.168.122.129" + ":6379");
        return Redisson.create(config); //返回实例
    }
}

启用分布式锁

信号量(如果遇到需要其他进程也能解锁的情况,请使用分布式信号量.)

信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0

@Autowired 
    RedissonClient redissonClient; 
    // RSemaphore 可以理解为分布式的信号量,它的作用是用来限制同时访问共享区域的线程数量。
   	//引入分布式信号量  进行限流
   RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);  //设置key
   //商品可以秒杀的数量作为信号量
   semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); // 设置value
redis可重入锁(Reentrant Lock)

锁其实也是一种资源,各线程争抢锁操作对应到redisson中就是争抢着去创建一个hash结构,谁先创建就代表谁获得锁;hash的名称为锁名,hash里面内容仅包含一条键值对,键为redisson客户端唯一标识+持有锁线程id,值为锁重入计数;给hash设置的过期时间就是锁的过期时间。

// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}


lock.unlock();
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();

看门狗:
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。并且加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认30s后自动删除

// Redisson同时还为分布式锁提供了异步执行的相关方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);


// `RLock`对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出`IllegalMonitorStateException`错误。

如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

读写锁

写锁没有释放,读锁无法读取(等待写锁释放)(保证一定可以读到最新的数据)修改期间是一个互斥锁,只能存在一个写锁,但可以多个读锁
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");  //获取锁

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);  //加上读锁
//实例
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");  //获取锁
    String s = "";
    RLock rlock = rwlock.readLock();  //定义为写锁
    try{
        rLock.lock(); //加锁
        s=redis.Template.opsForValue().get("wruteValue"); // 拿到数据
    }catch(){
        //xxxxx
    }finally{
        rLock.unlock();//释放锁!
    }

// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);  //加上写锁
//实例
 RReadWriteLock rwlock = redisson.getReadWriteLock("rw-lock");  //获取锁
    String s = "";
    RLock rlock = rwlock.writeLock();  //定义为写锁
    try{
        rLock.lock(); //加锁
        s = UUID.randomUUID().toString(); // 随机一个文本
        redis.Template.opsForValue().set("wruteValue",s);
    }catch(){
        //xxxxx
    }finally{
        rLock.unlock();//释放锁!
    }



// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

redis 缓存能永久保存数据吗 redisson 缓存_分布式_03

闭锁

以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); //拿到闭锁对象
latch.trySetCount(1); //设置次数
latch.await(); // 阻塞 等待减为0

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");  //拿到闭锁对象
latch.countDown();

总结

这就是利用Redis或Ression做分布式缓存中一些应用