集成框架 -- Redission

  • 前言
  • Redisson
  • 准备工作
  • 常用的几把锁
  • 可重入锁 -- 大家常用的分布式锁
  • 公平锁
  • 联锁
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)
  • 信号量(Semaphore)
  • 可过期性信号量(PermitExpirableSemaphore)
  • 闭锁(CountDownLatch)
  • 关于lock 看门狗源码解析
  • lua语言
  • 项目使用
  • 配置
  • SpringBoot启动器
  • 配置单机运行
  • 工具类
  • 直接使用
  • 附 redis 工具类
  • 关于分布式锁 缓存一致性问题闲聊


前言

随着开发量的增加 本地锁已经不能满足架构的集群开发情况,这种情况下,系统需要再集成一些高级锁,同时能够支撑分布式锁,再上一篇的文章中,关于redis是可以实现分布式锁的,但是关于超时时间有很多影响数量,关于超时还要进行一些双删的逻辑,急需要一种可以集成在分布式框架。

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

关于Redisson项目的详细介绍可以在官方网站找到。

每个Redis服务实例都能管理多达1TB的内存。

能够完美的在云计算环境里使用,并且支持AWS ElastiCache主备版AWS ElastiCache集群版Azure Redis Cache和阿里云的云数据库Redis

以下是Redisson的结构:

Redisson作为独立节点 可以用于独立执行其他节点发布到分布式执行服务 和 分布式调度任务服务 里的远程任务。

redission如何使用 redis redission_spring

如果你现在正在使用其他的RedisJava客户端,那么Redis命令和Redisson对象匹配列表 能够帮助你轻松的将现有代码迁徙到Redisson框架里来。

Redisson底层采用的是Netty 框架。支持Redis 2.8以上版本,支持Java1.6+以上版本。

准备工作

redission如何使用 redis redission_redis_02

组装常用jar

整合redission 作为分布锁框架

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.5</version>
</dependency>

常用的几把锁

可重入锁 – 大家常用的分布式锁

基于RedisRedisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)RxJava2标准的接口。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断延长锁有效期。默认情况下,看门狗检查锁超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

RLock对象完全符合JavaLock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出``IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore对象.

公平锁

基于RedisRedisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

联锁

分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

红锁(RedLock)

红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同Redisson实例

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

读写锁(ReadWriteLock)

可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

信号量(Semaphore)

基于RedisRedisson的分布式信号量Semaphore

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

可过期性信号量(PermitExpirableSemaphore)

基于RedisRedisson可过期性信号量是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。

RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);

闭锁(CountDownLatch)

基于Redisson分布式闭锁CountDownLatch

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

从上述中的示例中,可以看到redisson基于lock本地锁分布式锁

redission如何使用 redis redission_Redis_03


redission如何使用 redis redission_spring boot_04


redission如何使用 redis redission_redission如何使用_05

关于lock 看门狗源码解析

redission如何使用 redis redission_Redis_06


redission如何使用 redis redission_redission如何使用_07

默认传了一个 -1 过期时间

@Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    	 // 当前线程
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // 获取锁 如果为空 获取到了
        if (ttl == null) {
            return;
        }
		// 否则开始循环
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);
		
		//-------------------------------------------- 无限循环处理
        try {
            while (true) {
            	// 尝试获取
                ttl = tryAcquire(leaseTime, unit, threadId);
			     // 为空获取成功跳出
                if (ttl == null) {
                    break;
                }
     //  --------------------------------------------------------------------
                // 等待消息
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

redission如何使用 redis redission_spring boot_08

不等于-1 使用传入时间

redission如何使用 redis redission_redission如何使用_09

redission如何使用 redis redission_spring boot_10


如果指定了时间走的逻辑 写了一个lua脚本 发给redis进行执行如果没指定

redission如何使用 redis redission_spring_11

redission如何使用 redis redission_redis_12


redission如何使用 redis redission_redission如何使用_13


如果不传 符合文中上文中默认的 30 秒 通过lua 发送给redis

如果成功了

redission如何使用 redis redission_redission如何使用_14

增加监听器,匿名类 operationComplete打开组件 如果不成功 return 返回, 如果成功

redission如何使用 redis redission_spring boot_15


成功进行调度 调用调度方法 scheduleExourationRenewal

redission如何使用 redis redission_spring_16

redission如何使用 redis redission_spring_17


判断是否有过期时间key 如果已经存在返回, 否则放到map里面维护 创建计时器任务 重写方法

定时任务什么时候执行,30秒过期

折叠一下

redission如何使用 redis redission_spring boot_18

redission如何使用 redis redission_redission如何使用_19

redission如何使用 redis redission_redis_20

redission如何使用 redis redission_spring boot_21


队列维护时间为

internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); = 30秒 / 3 也就是 10 秒 单位毫秒

redission如何使用 redis redission_spring_22


过期时间中如果过了3分之一执行定时任务任然线程还在

redission如何使用 redis redission_spring boot_23


再调用自己续期

如果没有了

redission如何使用 redis redission_Redis_24


任务取消

lua语言

lua语言Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

  • 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
  • 可扩展: Lua提供了非常易于使用的扩展接口和机制

应用场景

  • 独立应用脚本
  • 游戏开发
  • Web 应用脚本
  • 扩展和数据库插件如:MySQL ProxyMySQL WorkBench

通过面描述因该知道是什么,反正我们也不写lua语言,所以知道为什么要用lua就可以了。关于为什么要在代码里拼接脚本lua 脚本 为了保证原子性打包发给redis进行处理

项目使用

配置

由于是基于redis之上的框架,连接方式只要连接redis就可以让redission架构再上面再封装即可

spring.redis.host= ${redis.host}
					spring.redis.port= 6379
					spring.redis.password=${redis.password}
					spring.redis.jedis.pool.max-active=30
					spring.redis.jedis.pool.max-wait=3000
					spring.redis.jedis.pool.max-idle=20
					spring.redis.jedis.pool.min-idle=2
					spring.redis.timeout=3000

SpringBoot启动器

<dependency>
			            <groupId>org.springframework.boot</groupId>
			            <artifactId>spring-boot-starter-data-redis</artifactId>
			        </dependency>
			
			        <dependency>
			            <groupId>org.redisson</groupId>
			            <artifactId>redisson</artifactId>
			            <version>3.7.5</version>
			        </dependency>

配置单机运行

import com.alibaba.genie.aftersale.common.util.LockUtil;
import com.alibaba.genie.aftersale.common.util.redisson.RedissonLocker;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;


/**
 * 〈功能描述〉<br>
 * ----- : 备用
 *
 * @date 2021/10/9 11:55
 */
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;

    /**
     * RedissonClient,单机模式
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        return Redisson.create(config);
    }

    @Bean
    public RedissonLocker redissonLocker(RedissonClient redissonClient) {
        RedissonLocker locker = new RedissonLocker(redissonClient);
        //设置LockUtil的锁处理对象
        LockUtil.setLocker(locker);
        return locker;
    }
}

工具类

import java.util.concurrent.TimeUnit;

/**
 * 〈功能描述〉<br>
 * ----- :
 *
 * @date 2021/10/9 11:56
 */
public interface Locker {

    /**
     * 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。
     *
     * @param lockKey
     */
    void lock(String lockKey);

    /**
     * 释放锁
     *
     * @param lockKey
     */
    void unlock(String lockKey);

    /**
     * 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
     *
     * @param lockKey
     * @param timeout
     */
    void lock(String lockKey, int timeout);

    /**
     * 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
     *
     * @param lockKey
     * @param unit
     * @param timeout
     */
    void lock(String lockKey, TimeUnit unit, int timeout);

    /**
     * 尝试获取锁,获取到立即返回true,未获取到立即返回false
     *
     * @param lockKey
     * @return
     */
    boolean tryLock(String lockKey);

    /**
     * 尝试获取锁,在等待时间内获取到锁则返回true,否则返回false,如果获取到锁,则要么执行完后程序释放锁,
     * 要么在给定的超时时间leaseTime后释放锁
     *
     * @param lockKey
     * @param waitTime
     * @param leaseTime
     * @param unit
     * @return
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;

    /**
     * 锁是否被任意一个线程锁持有
     *
     * @param lockKey
     * @return
     */
    boolean isLocked(String lockKey);
}
import java.util.concurrent.TimeUnit;

/**
 * 〈功能描述〉<br>
 * ----- :
 *
 * @date 2021/10/9 11:53
 */
@Data
public class RedissonLocker implements Locker {


	// 配置构造方法注入
    private RedissonClient redissonClient;

    public RedissonLocker(RedissonClient redissonClient) {
        super();
        this.redissonClient = redissonClient;
    }

    @Override
    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();

    }

    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void lock(String lockKey, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
    }

    @Override
    public void lock(String lockKey, TimeUnit unit, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
    }


    @Override
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }

    @Override
    public boolean tryLock(String lockKey, long waitTime, long leaseTime,
                           TimeUnit unit) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock(waitTime, leaseTime, unit);
    }

    @Override
    public boolean isLocked(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
public class LockUtil {
    private static Locker locker;

    /**
     * 设置工具类使用的locker
     *
     * @param locker
     */
    public static void setLocker(Locker locker) {
        LockUtil.locker = locker;
    }

    /**
     * 获取锁
     *
     * @param lockKey
     */
    public static void lock(String lockKey) {
        locker.lock(lockKey);
    }

    /**
     * 释放锁
     *
     * @param lockKey
     */
    public static void unlock(String lockKey) {
        locker.unlock(lockKey);
    }

    /**
     * 获取锁,超时释放
     *
     * @param lockKey
     * @param timeout
     */
    public static void lock(String lockKey, int timeout) {
        locker.lock(lockKey, timeout);
    }

    /**
     * 获取锁,超时释放,指定时间单位
     *
     * @param lockKey
     * @param unit
     * @param timeout
     */
    public static void lock(String lockKey, TimeUnit unit, int timeout) {
        locker.lock(lockKey, unit, timeout);
    }

    /**
     * 尝试获取锁,获取到立即返回true,获取失败立即返回false
     *
     * @param lockKey
     * @return
     */
    public static boolean tryLock(String lockKey) {
        return locker.tryLock(lockKey);
    }

    /**
     * 尝试获取锁,在给定的waitTime时间内尝试,获取到返回true,获取失败返回false,获取到后再给定的leaseTime时间超时释放
     *
     * @param lockKey
     * @param waitTime
     * @param leaseTime
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
                                  TimeUnit unit) throws InterruptedException {
        return locker.tryLock(lockKey, waitTime, leaseTime, unit);
    }

    /**
     * 锁释放被任意一个线程持有
     *
     * @param lockKey
     * @return
     */
    public static boolean isLocked(String lockKey) {
        return locker.isLocked(lockKey);
    }
}

直接使用

LockUtil.lock("建", 超时时间4);

附 redis 工具类

package com.alibaba.genie.aftersale.common.util;

import com.alibaba.genie.aftersale.biz.repository.UserRepository;
import com.alibaba.genie.aftersale.dal.model.UserDO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Component;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 〈功能描述〉<br>
 * ----- :
 *
 * @date 2021/3/16 13:36
 */

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    UserRepository userRepository;

    /**
     * 判断key 是否存在,不存在创建key 自增,存在key 自增
     *
     * @param key
     * @return
     */
    public Long incrementChecker(String key) {
        // 判断key是否存在
        if (!hasKey(key)) {
            set(key, 0);
        }
        return increment(key);
    }


    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据key获取自增key的redis值
     *
     * @param key
     * @return
     */
    public Long getIncr(String key) {
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, Objects.requireNonNull(redisTemplate.getConnectionFactory()));
        return entityIdCounter.getAndIncrement();
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 自增
     *
     * @param key
     * @return 自增后的值
     */
    public Long increment(String key) {
        return this.redisTemplate.opsForValue().increment(key);
    }


    /**
     * 自增 num
     *
     * @param key
     * @return 自增后的值
     */
    public Long increment(String key, long num) {
        return this.redisTemplate.opsForValue().increment(key, num);
    }


    /**
     * 获取不可变 常量工号 真实姓名
     *
     *
     * @param workId
     * @return
     */
    public String getNick(String workId) {
        if (get(workId) == null) {
            UserDO userInfoByWorkId = userRepository.getUserInfoByWorkId(workId);
            if (userInfoByWorkId != null && StringUtils.isNotBlank(userInfoByWorkId.getNickName())) {
                set(workId, userInfoByWorkId.getNickName());
                return userInfoByWorkId.getNickName();
            }

        }

        return (String) get(workId);
    }
}
try {
             LockUtil.lock("createLock", TimeUnit.SECONDS, 5);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            LockUtil.unlock("createByCooperateLock");
        }

关于分布式锁 缓存一致性问题闲聊

题外篇, 说说个人理解 一般解决莫过于双删, 以及过期时间, 但是这都可能会存在最终一致性问题,个人常用解决办法, 通过mq 自己消费自己进行延时双删,或者通过服务器加入中间件,默认一般都会有毕竟项目用检索同步的比较多。而且大型项目hbase也常用,关于数据同步问题可以通过binlog日志 服务器嵌入 Canal 同步redis 删除一次搭建再也不管。