Redis进阶

85 阅读46分钟

=====缓存穿透&击穿&雪崩=====

一、缓存穿透

  • 缓存穿透:缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效(只有数据库查到了,才会让redis缓存,但现在的问题是查不到),会频繁的去访问数据库。
  • 常见的结局方案有两种
  1. 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:额外的内存消耗,可能造成短期的不一致
  1. 布隆过滤器
    • 优点:内存占用少,没有多余的key
    • 缺点:实现复杂,可能存在误判

缓存空对象思路分析:当我们客户端访问不存在的数据时,会先请求redis,但是此时redis中也没有数据,就会直接访问数据库,但是数据库里也没有数据,那么这个数据就穿透了缓存,直击数据库。但是数据库能承载的并发不如redis这么高,所以如果大量的请求同时都来访问这个不存在的数据,那么这些请求就会访问到数据库,简单的解决方案就是哪怕这个数据在数据库里不存在,我们也把这个这个数据存在redis中去(这就是为什么说会有额外的内存消耗),这样下次用户过来访问这个不存在的数据时,redis缓存中也能找到这个数据,不用去查数据库。可能造成的短期不一致是指在空对象的存活期间,我们更新了数据库,把这个空对象变成了正常的可以访问的数据,但由于空对象的TTL还没过,所以当用户来查询的时候,查询到的还是空对象,等TTL过了之后,才能访问到正确的数据,不过这种情况很少见罢了。

布隆过滤思路分析:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,根据哈希思想去判断当前这个要查询的数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库里一定会存在这个数据,从数据库中查询到数据之后,再将其放到redis中。如果布隆过滤器判断这个数据不存在,则直接返回。这种思想的优点在于节约内存空间,但存在误判,误判的原因在于:布隆过滤器使用的是哈希思想,只要是哈希思想,都可能存在哈希冲突。

缓存空对象实现:

@Override
public Result queryById(Long id) {
//先从Redis中查,这里的常量值是固定的前缀 + 店铺id
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//如果不为空(查询到了),则转为Shop类型直接返回
if (StrUtil.isNotBlank(shopJson)) {
    Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    return Result.ok(shop);
}
//如果shopjson对象不为空,则就是空字符串,说明是我们缓存的空数据
if (shopjson != null) {
    return Result.fail("店铺不存在!!");
}
//否则去数据库中查
Shop shop = getById(id);
//查不到,则将空字符串写入Redis
if (shop == null) {
    //这里的常量值是2分钟
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    return Result.fail("店铺不存在!!");
}
//查到了则转为json字符串
String jsonStr = JSONUtil.toJsonStr(shop);
//并存入redis,设置TTL
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//最终把查询到的商户信息返回给前端
return Result.ok(shop);
}

二、缓存雪崩

缓存雪崩是指在同一时间段,大量缓存的key同时失效,或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值,让其在不同时间段分批失效;
  • 利用Redis集群提高服务的可用性(使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。 );
  • 给缓存业务添加降级限流策略;
  • 给业务添加多级缓存(浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源(ajax查询数据)时,访问服务端;请求到达Nginx后,优先读取Nginx本地缓存;如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat);如果Redis查询未命中,则查询Tomcat;请求进入Tomcat后,优先查询JVM进程缓存;如果JVM进程缓存未命中,则查询数据库)。

三、缓存击穿

缓存击穿也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,那么无数请求访问就会在瞬间给数据库带来巨大的冲击。

举个不太恰当的例子:一件秒杀中的商品的key突然失效了,大家都在疯狂抢购,那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿。

常见的解决方案有两种:

  1. 互斥锁
  2. 逻辑过期

逻辑分析:假设线程1在查询缓存之后未命中,本来应该去查询数据库,重建缓存数据,完成这些之后,其他线程也就能从缓存中加载这些数据了。但是在线程1还未执行完毕时,又进来了线程2、3、4同时来访问当前方法,那么这些线程都不能从缓存中查询到数据,那么他们就会在同一时刻访问数据库,执行SQL语句查询,对数据库访问压力过大。

1.解决方案一:互斥锁

  • 利用锁的互斥性,假设线程过来,只能一个人一个人的访问数据库,从而避免对数据库频繁访问产生过大压力,但这也会影响查询的性能,将查询的性能从并行变成了串行,我们可以采用tryLock方法+double check(在当前线程获取锁后,查询数据库之前,要再次查询redis看是否有数据) 来解决这个问题。
  • 线程1在操作的时候,拿着锁把房门锁上了,那么线程2、3、4就不能都进来操作数据库,只有1操作完了,把房门打开了,此时缓存数据也重建好了,线程2、3、4直接从redis中就可以查询到数据。

2.解决方案二:逻辑过期

逻辑过期方案存在数据不一致

  • 方案分析:我们之所以会出现缓存击穿问题,主要原因是在于我们对key设置了TTL,如果我们不设置TTL,那么就不会有缓存击穿问题,但是不设置TTL,数据又会一直占用我们的内存,所以我们可以采用逻辑过期方案。
  • 我们之前是TTL设置在redis的value中,注意:这个过期时间并不会直接作用于Redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断当前数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的进程他会开启一个新线程去进行之前的重建缓存数据的逻辑,直到新开的线程完成者逻辑之后,才会释放锁,而线程1直接进行返回,假设现在线程3过来访问,由于线程2拿着锁,所以线程3无法获得锁,线程3也直接返回数据(但只能返回旧数据,牺牲了数据一致性,换取性能上的提高),只有等待线程2重建缓存数据之后,其他线程才能返回正确的数据。
  • 这种方案巧妙在于,异步构建缓存数据,缺点是在重建完缓存数据之前,返回的都是脏数据。


3.对比互斥锁与逻辑删除

  • 互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,只是加了一把锁而已,也没有其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁的情况,就可能死锁,所以只能串行执行,性能会受到影响。
  • 逻辑过期方案:线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构缓存数据,但是在重构数据完成之前,其他线程只能返回脏数据,且实现起来比较麻烦。
解决方案优点缺点
互斥锁没有额外的内存消耗 保证一致性 实现简单线程需要等待,性能受影响 可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性 有额外内存消耗 实现复杂

4.利用互斥锁解决缓存击穿问题

  • 操作锁的代码核心思路就是利用redis的setnx方法来表示获取锁,如果redis没有这个key,则插入成功,返回1,如果已经存在这个key,则插入失败,返回0。在StringRedisTemplate中返回true/false,我们可以根据返回值来判断是否有线程成功获取到了锁。
  • tryLock:
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    //避免返回值为null,我们这里使用了BooleanUtil工具类
    return BooleanUtil.isTrue(flag);
}

unLock:

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

核心代码(无double check):

@Override
public Shop queryWithMutex(Long id) {
//先从Redis中查,这里的常量值是固定的前缀 + 店铺id
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//如果不为空(查询到了),则转为Shop类型直接返回
if (StrUtil.isNotBlank(shopJson)) {
    Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    return shop;
}
//命中缓存的空对象
if (shopJson != null) {
    return null;
}
Shop shop = null;
try {
    //否则去数据库中查
    boolean flag = tryLock(LOCK_SHOP_KEY + id);
    if (!flag) {
        Thread.sleep(50);
        return queryWithMutex(id); //休眠50ms进行递归重试
    }
    
    //此处可添加double check,redis有数据直接返回,无数据再往下走
    
    //查不到,则将空值写入Redis
    shop = getById(id);
    if (shop == null) {
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }
    //查到了则转为json字符串
    String jsonStr = JSONUtil.toJsonStr(shop);
    //并存入redis,设置TTL
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //最终把查询到的商户信息返回给前端
} catch (InterruptedException e) {
    throw new RuntimeException(e);
} finally {
    unlock(LOCK_SHOP_KEY + id);
}
return shop;
}

5.利用逻辑过期解决缓存击穿问题

需提前进行redis缓存预热,填充热点数据

  • 需求:根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题。
  • 思路分析:当用户开始查询redis时,判断是否命中;
  • 如果没有命中则直接返回空数据,不查询数据库;
  • 如果命中,则将value取出,判断value中的过期时间是否满足
    • 如果没有过期,则直接返回redis中的数据;
    • 如果过期,则在开启独立线程后,直接返回之前的数据,独立线程去重构数据,重构完成后再释放互斥锁。

  • 封装数据:因为现在redis中存储的数据的value需要带上过期时间,此时要么你去修改原来的实体类,要么新建一个类包含原有的数据和过期时间。

1.这里我们选择新建一个实体类,包含原有数据(使用泛型T)和过期时间,这样对原有的代码没有侵入性

@Data
public class RedisData<T> {
    private LocalDateTime expireTime;
    private T data;
}

2.核心代码

//这里需要声明一个线程池,因为下面我们需要新建一个现成来完成重构缓存
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

@Override
public Shop queryWithLogicalExpire(Long id) {
//1. 从redis中查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2. 如果未命中,则返回空
if (StrUtil.isBlank(json)) {
    return null;
}
//3. 命中,将json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
//3.1 将data转为Shop对象
JSONObject shopJson = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//3.2 获取过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//4. 判断是否过期
if (LocalDateTime.now().isBefore(time)) {
    //5. 未过期,直接返回商铺信息
    return shop;
}
//6. 过期,尝试获取互斥锁
boolean flag = tryLock(LOCK_SHOP_KEY + id);
//7. 获取到了锁
if (flag) {
    //8. 开启独立线程
    CACHE_REBUILD_EXECUTOR.submit(() -> {
        try {
            this.saveShop2Redis(id, LOCK_SHOP_TTL);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            unlock(LOCK_SHOP_KEY + id);
        }
    });
    //9. 当前直接返回商铺信息
    return shop;
}
//10. 未获取到锁,直接返回商铺信息
return shop;
}

3.saveShop2Redis方法重新写入数据

public void saveShop2Redis(Long id, Long expirSeconds) {
    Shop shop = getById(id);
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
    //使用同一个key,覆盖旧数据
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

=====分布式锁=====

一、分布式锁原理

1.基本原理和实现方式对比

  • 分布式锁:满足分布式系统或集群模式下多线程可见并且可以互斥的锁;
  • 分布式锁的核心思想就是让大家共用同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路。

那么分布式锁应该满足一些什么条件呢?

  1. 可见性:多个线程都能看到相同的结果。
  2. 互斥:互斥是分布式锁的最基本条件,使得程序串行执行
  3. 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  4. 高性能:由于加锁本身就让性能降低,所以对于分布式锁需要他较高的加锁性能和释放锁性能
  5. 安全性:安全也是程序中必不可少的一环

注意:这里说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思。


常见的分布式锁有三种

  1. MySQL:MySQL本身就带有锁机制,但是由于MySQL的性能一般,所以采用分布式锁的情况下,使用MySQL作为分布式锁比较少见。
  2. Redis:Redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都是用Redis或者Zookeeper作为分布式锁,利用SETNX这个方法,如果插入Key成功,则表示获得到了锁,如果有人插入成功,那么其他人就回插入失败,无法获取到锁,利用这套逻辑完成互斥,从而实现分布式锁。
  3. Zookeeper:Zookeeper也是企业级开发中较好的一种实现分布式锁的方案,但本文是学Redis的,所以这里就不过多阐述了。
****MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

**

**

2.Redis分布式锁误删情况说明

  • 逻辑说明
    • 持有锁的线程1在锁的内部出现了阻塞,导致他的锁TTL到期,自动释放;
    • 此时线程2也来尝试获取锁,由于线程1已经释放了锁,所以线程2可以拿到;
    • 但是现在线程1阻塞完了,继续往下执行,要开始释放锁了;
    • 那么此时就会将属于线程2的锁释放,这就是误删别人锁的情况。
  • 解决方案
    • 在每个线程释放锁的时候,都判断一下这个锁是不是自己的,如果不属于自己,则不进行删除操作。
    • 假设还是上面的情况,线程1阻塞,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1阻塞完了,继续往下执行,开始删除锁,但是线程1发现这把锁不是自己的,所以不进行删除锁的逻辑,当线程2执行到删除锁的逻辑时,如果TTL还未到期,则判断当前这把锁是自己的,于是删除这把锁。

3.Redis分布式锁解决误删问题

  • 在获取锁的时候存入线程标识(用UUID标识,在一个JVM中,ThreadId一般不会重复,但是我们现在是集群模式,有多个JVM,多个JVM之间可能会出现ThreadId重复的情况),在释放锁的时候先获取锁的线程标识,判断是否与当前线程标识一致:
    • 如果一致则释放锁,
    • 如果不一致则不释放锁。
  • 核心逻辑:在存入锁的时候,放入自己的线程标识,在删除锁的时候判断当前这把锁是不是自己存入的:
    • 如果是,则进行删除,
    • 如果不是,则不进行删除。
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

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

4.分布式锁的原子性问题

更为极端的误删逻辑说明:

  • 假设线程1已经获取了锁,在判断标识一致之后,准备释放锁的时候,又出现了阻塞(例如JVM垃圾回收机制),于是锁的TTL到期了,自动释放了,那么现在线程2趁虚而入,拿到了一把锁,但是线程2的逻辑还没执行完,线程1阻塞完毕要执行删除锁的逻辑,且在阻塞前线程1已经判断了标识一致,所以现在线程1把线程2的锁给删了,这就是删锁时的原子性问题。因为线程1的拿锁,判断标识,删锁,不是原子操作,所以我们要防止刚刚的情况。

5.Lua脚本解决多条命令原子性问题

  • Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
  • 这里重点介绍Redis提供的调用函数,我们可以使用Lua去操作Redis,而且还能保证它的原子性,这样就可以实现拿锁判断标识删锁是一个原子性动作了。
-- 这里的KEYS[1]就是传入锁的key
-- 这里的ARGV[1]就是线程标识
-- 比较锁中的线程标识与线程标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
  -- 一致则释放锁
  return redis.call('del', KEYS[1])
end
return 0

6.利用Java代码调用Lua脚本改造分布式锁

//DefaultRedisScript用于封装lua脚本,泛型类型为返回值类型。
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
}

@Override
public void unlock() {
    stringRedisTemplate.execute(UNLOCK_SCRIPT,
                                Collections.singletonList(KEY_PREFIX + name),
                                ID_PREFIX + Thread.currentThread().getId());
}

execute底层源码:

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
    return this.scriptExecutor.execute(script, keys, args);
}

二、Redisson分布式锁

1.前言

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

基于SETNX实现的分布式锁存在以下问题:

  • 1.重入问题
    • 重入问题是指同一线程无法多次获取同一把锁-当方法a调用方法b,b中要获取锁时无法获取可重入锁的意义在于防止死锁。
  • 2.不可重试
    • 我们编写的分布式锁只能尝试一次,失败了就返回false,没有重试机制。但合理的情况应该是:当线程获取锁失败后,他应该能再次尝试获取锁。
  • 3.超时释放
    • 我们在加锁的时候增加了TTL,这样我们可以防止死锁,但是如果卡顿(阻塞)时间太长,也会导致锁的释放。虽然我们采用Lua脚本来防止删锁的时候,误删别人的锁,但现在的新问题是没锁住,也有安全隐患。
  • 4.主从一致性
    • 如果Redis提供了主从集群,那么当我们向集群写数据时,主机需要异步的将数据同步给从机,万一在同步之前,主机宕机了(主从同步存在延迟,虽然时间很短,但还是发生了),那么又会出现死锁问题。

Redis提供了分布式锁的多种多样功能:

  1. 可重入锁(Reentrant Lock)
  2. 公平锁(Fair Lock)
  3. 联锁(MultiLock)
  4. 红锁(RedLock)
  5. 读写锁(ReadWriteLock)
  6. 信号量(Semaphore)
  7. 可过期性信号量(PermitExpirableSemaphore)
  8. 闭锁(CountDownLatch)

2.Redisson入门

导入依赖

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

配置Redisson客户端

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        .setAddress("redis://101.XXX.XXX.160:6379")
        .setPassword("root");
        return Redisson.create(config);
    }
}

使用Redisson分布式锁

@Resource
private RedissonClient redissonClient;

@Test
void testRedisson() throws InterruptedException {
//获取可重入锁
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,三个参数分别是:获取锁的最大等待时间(期间会重试),锁的自动释放时间,时间单位
boolean success = lock.tryLock(1,10, TimeUnit.SECONDS);
//判断获取锁成功
if (success) {
    try {
        System.out.println("执行业务");
    } finally {
        //释放锁
        lock.unlock();
    }
  }
}

**
**tryLock参数:

  • 有参:获取锁的最大等待时间(期间会重试,自旋), 锁的自动释放``时间, 时间单位。
  • 无参:全部默认,即 -1(无自旋时间,获取锁失败就为false), 30, seconds(超时时间30s)。
  • 如果设置不设置有效期,会开启看门狗机制,默认过期时间30s,当业务未结束时,锁过期会自动续约。

3.Redisson可重入锁原理

JUC的Lock锁中,他是借助于等增的一个voaltile修饰的state变量来记录重入的状态的:

  • 如果当前没有人持有这把锁,那么state = 0;如果人持有这把锁,那么state = 1;如果持有这把锁的人再次持有这把锁,那么state会+1,也是重入一次就+1,释放一次就-1,直至减到0,表示这把锁没有被人持有。

redisson中,我们也支持可重入锁:

  • 在分布式锁中,它采用hash结构来存储锁,其中外层key表示这把锁是否存在,内层key则记录当前这把锁被哪个线程持有。

method1在方法内部调用method2,method1和method2出于同一个线程,那么method1已经拿到一把锁了,想进入method2中拿另外一把锁,必然是拿不到的,于是就出现了死锁。

@Resource
private RedissonClient redissonClient;

private RLock lock;

@BeforeEach
void setUp() {
    lock = redissonClient.getLock("lock");
}

@Test
void method1() {
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,1");
        return;
    }
    try {
        log.info("获取锁成功");
        method2();
    } finally {
        log.info("释放锁,1");
        lock.unlock();
    }
}

void method2() {
    RLock lock = redissonClient.getLock("lock");
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,2");
        return;
    }
    try {
        log.info("获取锁成功,2");
    } finally {
        log.info("释放锁,2");
        lock.unlock();
    }
}
  • 所以我们需要额外判断,method1和method2是否处于同一线程,如果是同一个线程,则可以拿到锁,但是state会+1,之后执行method2中的方法,释放锁,释放锁的时候也只是将state进行-1,只有减至0,才会真正释放锁;
  • 由于我们需要额外存储一个state,所以用字符串型SET NX EX是不行的,需要用到Hash结构,但是Hash结构又没有NX这种方法,所以我们需要将原有的逻辑拆开,进行手动判断。

4.使用Lua脚本实现Redisson

为了保证原子性,所以流程图中的业务逻辑也是需要我们用Lua来实现的

  • 获取锁的逻辑
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 锁不存在
if (redis.call('exists', key) == 0) then
  -- 获取锁并添加线程标识,state设为1
  redis.call('hset', key, threadId, '1');
  -- 设置锁有效期
  redis.call('expire', key, releaseTime);
  return 1; -- 返回结果
end;
-- 锁存在,判断threadId是否为自己
if (redis.call('hexists', key, threadId) == 1) then
  -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
  redis.call('hincrby', key, thread, 1);
  -- 设置锁的有效期
  redis.call('expire', key, releaseTime);
  return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
  • 释放锁的逻辑
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];
-- 如果锁不是自己的
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 直接返回
end;
-- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1
local count = redis.call('hincrby', key, threadId, -1);
-- 判断重入次数为多少
if (count > 0) then
    -- 大于0,重置有效期
    redis.call('expire', key, releaseTime);
    return nil;
else
    -- 否则直接释放锁
    redis.call('del', key);
    return nil;
end;

=====高级数据结构=====

一、Bitmaps

位图就是一个用二进制位(0和1)来表示数据的结构,最大容量为223。可以把它想象成一排开关,每个开关只能

是开(1)或者关(0)。这些开关排成一行,从左到右编号,编号从0开始。

目的就是操作某一个位置的数据变成1或者 0。

整体效果是下面这种:

主要操作命令:

setbit jichi 4 1   //把4位设成1
getbit jichi 4 //获取4位的值
bitcount jichi //获取bitmap里有几个1

举个例子:

假设有一个用户签到系统,我们可以用 bitmap 来记录每个用户每天是否签到。比如,一个

月有30天,我们可以用30个位来表示这个月的签到情况,我们就可以如此设计。

第1天签到:第0位设为1。第2天没签到:第1位设为0。第3天签到:第2位设为1。以此类

推...

这个例子就用上面三个命令即可完成,setbit 设置签到位置,getbit 判断某一天有没有签

到,bitcount 获取总共签了多少次到。

假设用户在第1天和第3天签到,那么 bitmap 的值就是下面这样的:

101000000000000000000000000000

为什么用 bitmap:

类似签到,活跃情况,这些场景。假设我们用数据库存储,可能是一条一条的,统计起来也费时和麻烦,如果使用 bitmap,可以进行非常快速的统计,并且bitmap 每个位只是二进制位,非常节省空间。扩展起来,其实比如判断用户有没有权限,假设把某个权限作为一个位置,新增作为1,删除作为 2,那么这种场景也是可以很快知道用户是否有权限的一种方式。

总之涉及单位置判断的,是否的场景,bitmap 比较靠谱。

二、布隆过滤器

1.布隆过滤器BloomFilter是什么

布隆过滤器BloomFilter是一种专门用来解决去重问题的高级数据结果。

实质就是一个大型位数组和几个不同的无偏hash函数,无偏表示分布均匀。由一个初值为零的bit数组和多个哈希函数组成,用来判断某个数据是否存在,它和HyperLogLog一样,不是那么的精准,存在一定的误判概率。

2.布隆过滤器BloomFilter能干嘛?

高效地插入和查询,占用空间少,返回的结果是不确定的,一个元素如果判断结果为存在,它不一定存在;不存在时,一定不存在。


因为不同的字符串的hashcode可能相同,布隆过滤器BloomFilter是根据hashcode判断的,如果某个hashcode存在,它对应的字符串不一定是你想要的那个字符串;但是,hashcode不存在时,你所要的字符串,肯定不存在。


布隆过滤器BloomFilter只能添加元素,不能删除元素。

这和上面提到的hashcode判定原理是一样的,相同hashcode的字符串会存储在一个index,删除时,是将某个index移除,此时,就可能移除拥有相同hashcode的不同字符串。

3.使用场景

①解决缓存穿透问题

一般情况下,先查询Redis缓存,如果Redis中没有,再查询MySQL。当数据库中也不存在这条数据时,每次查询都要访问数据库,这就是缓存穿透。

在Redis前面添加一层布隆过滤器,请求先在布隆过滤器中判断,如果布隆过滤器不存在时,直接返回,不再反问Redis和MySQL。

如果布隆过滤器中存在时,再访问Redis,再访问数据库。

②黑名单

如果黑名单非常大,上千万了,存放起来很耗费空间,在布隆过滤器中实现黑名单功能,是一个很好的选择。

网页爬虫对URL的去重,避免爬取相同的URL地址

三、HyperLongLong

HyperLogLog 用于计算数据集中不重复元素的数量,是 Redis 提供的一种基数统计的数据结构,。

当我们需要统计大量数据中有多少不同的元素时,直接存储所有元素会占用大量内存。例如,统

计一个网站一天内有多少不同的IP地址访问。如果直接存储所有IP地址,内存消耗会非常大

HyperLogLog通过巧妙的数学方法,可以在很小的内存占用下,提供一个非常接近的估算值。在

Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素

的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

什么是基数:比如数据集 {1,3,5,7,5,7,8},那么这个数据集的基数集为{1,3,5 ,7,8},基数(不重复元素)为5个。基

数估计就是在误差可接受的范围内,快速计算基数。


常用命令:

HyperLogLog 在 Redis 中以字符串的形式存在,但是只能作为计数器来使用,并不能获取到集合

的原始数据。

主要涉及三个命令:

添加元素

PFADD key element1 element2......

估值基数

PFCOUNT key

合并多个HyperLongLong

PFMERG destkey sourcekey1 sourcekey2...

应用场景:

凡是大量的数据下,统计不同数据的数量的情况都可以使用,非常的方便,同时要接受误差的场

景。比如:

网站访问统计: 估算鸡翅 club 网站每天有多少独立访客

日志分析: 估算日志文件中有多少不同的错误类型。

四、Geospatial lndexes

Geo数据指的是与地理位置相关的数据。简单来说,就是关于“东西在哪里”的数据。它可以描

述物体的位置、形状和关系,比如城市的坐标、商店的位置、路线的路径等等。

有主要的三个要素,经度,纬度,和位置名称。

比如我所在的位置:

GEOADD ssm 16.281231 37.1231241 alibaba

常用命令:

添加地理位置

GEOADD key longitude latitude member [longitude latitude member
例如:GEOADD cities 116.4074 39.9042 "Beijing

获取地理位置

GEOPOs key member
例如:GEOPos cities "alibaba'
会返回
116.4074
39.9042

计算距离

GEoDIST key member1 member2 unit]
例如:GEODIST cities“Beijing""shanghai”km(计算北京和上海之间的距离,单位为公里)

查找附近位置

GEORADIUs key longitude latitude radius unit
例如:GEORADIUS cities 116.4074 39.9042 100 km(查找北京附近100公里内的所有城市)

查找某个位置的附近位置

GEORADIUSBYMEMBER key member radiusunit]
例如:GEORADIUSBYMEMBER cities“Beijing”100 km(查找北京附近100公里内的所有城市)

georadius 以给定的经纬度为中心,返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

georadiusbymember和GEORADIUS命令一样,都可以找出位于指定范围内的元素,但georadiusbymember 的中心点是由给定的位置元素决定的,而不是使用经度和纬度来决定中心点。


应用场景:

附近的人: 比如类似微信的附近的人,以自己为中心,找其他的人,这种场景,就可以使用GEORADIUS。

基于地理位置推荐: 比如推荐某个位置附近的餐厅,都可以实现。

计算距离: 比如当你购物的时候,美团外卖会告诉你商家距您多远,也可以通过 geo 来进行实现。

=====持久化机制=====

主要的持久化方式有两种:RDB(Redis Database)和AOF(Append Only File),Redis 4.0引入了混合持久化模式。

一、RDB(Redis Database)

RDB持久化方式会在指定的时间间隔内生成数据集的快照,并将其保存到磁盘上。

这个快照文件的默认名称是dump.rdb。

RDB的配置可以在redis.conf文件中进行。例如:

save 900 1 #如果900秒(15分钟)内至少有1个键发生变化,就触发一次RDB快照
save 300 10 #如果300秒(5分钟)内至少有10个键发生变化,就触发一次RDB快照
save 60 10000 # 如果60秒(1分钟)内至少有10000个键发生变化,就触发一次RDB快照

优点:

1、RDB文件是一个紧凑的二进制文件,可以很容易地进行备份。

2、 恢复速度快,适合用于灾难恢复。

3、对Redis性能影响较小,因为生成RDB文件的工作是在子进程中进行的。

缺点:

1、数据持久化的频率较低,可能会丢失最近一次快照之后的数据。

2、生成RDB快照时,可能会消耗较多的CPU和内存资源。

二、AOF (Append only File)

AOF持久化方式记录每一个写操作到日志文件中(默认名称是appendonly.aof)。Redis会将这些写操作以追加的方式写入到AOF文件中。

AOF的配置可以在redis.conf文件中进行。例如:

appendonly yes #启用AOF持久化
appendfilename "appendonly.aof"
appendfsynceverysec #每秒钟同步一次AOF文件
#其他选项:
#appendfsync always #每个写操作都同步到AOF文件,性能较差但数据最安全
# appendfsync no #由操作系统决定何时同步,性能最好但数据安全性较差

优点:

1、数据恢复更可靠,AOF可以记录每一个写操作,数据丢失风险较小。

2、AOF文件是可读的文本文件,方便分析和调试。

缺点:

1、 AOF文件比RDB文件大,恢复速度较慢。

2、持久化频率高时,可能会影响Redis性能。

3、需要定期进行AOF重写(rewrite),以避免文件过大。

三、混合持久化(Hybrid Persistence)

混合持久化模式结合了RDB和AOF的优点。在Redis4.0及以上版本中,混合持久化模式在生成新的AOF文件时,会首先创建一个RDB快照,然后在快照之后追加AOF日志。这种方式可以在保证数据恢复速度的同时,减少数据丢失的风险。混合持久化的配置可以在redis.conf文件中进行。

aof-use-rdb-preamble yes #启用混合持久化模式

优点:结合了RDB和AOF的优点,既能快速恢复数据,又能减少数据丢失的风险。

四、选择建议

RDB:适用于对数据一致性要求不高,但需要快速恢复数据的场景,例如缓存服务器

AOF:适用于对数据一致性要求高的场景,例如金融交易系统。

混合持久化:适用于需要综合考虑数据恢复速度和数据一致性的场景。

=====主从&哨兵&集群模式=====

一、主从模式

在上一篇文章中,我们了解了Redis两种不同的持久化方式,Redis服务器通过持久化,把Redis内存中持久化到硬盘当中,当Redis宕机时,我们重启Redis服务器时,可以由RDB文件或AOF文件恢复内存中的数据。

不过持久化后的数据仍然只在一台机器上,因此当硬件发生故障时,比如主板或CPU坏了,这时候无法重启服务器,有什么办法可以保证服务器发生故障时数据的安全性?或者可以快速恢复数据呢?想做到这一点,我们需要再了解Redis另外一种机制:主从复制

1.什么是主从复制

Redis的主从复制机制是指可以让从节点服务器(slave)能精确复制主节点服务器(master)的数据,如下图所示:


上面的图表示的是一台master服务器与slave服务器的情况,其实一台master服务器也可以对应多台slave服务器,如下图所示:

另外,slave服务器也可以有自己的slave服务器,这样的服务器称为sub-slave,而这些sub-slave通过主从复制最终数据也能与master保持一致,如下图所示:

2.主从复制的方式和工作原理

Redis的主从复制是异步复制,异步分为两个方面,一个是master服务器在将数据同步到slave时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的。

复制方式

Redis主从复制分为以下三种方式:

  • master服务器与slave服务器正常连接时,master服务器会发送数据命令流给slave服务器,将自身数据的改变复制到slave服务器。
  • 当因为各种原因master服务器与slave服务器断开后,slave服务器在重新连上master服务器时会尝试重新获取断开后未同步的数据即部分同步,或者称为部分复制。
  • 如果无法部分同步(比如初次同步时主节点有大量数据,从节点无数据),则会请求进行全量同步,这时master服务器会将自己的rdb文件发送给slave服务器进行数据同步,并记录同步期间的其他写入,再发送给slave服务器,以达到完全同步的目的,这种方式称为全量复制。

工作原理

master服务器会记录一个replicationId的伪随机字符串,用于标识当前的数据集版本,还会记录一个当数据集的偏移量offset,不管master是否有配置slave服务器,replicationIdoffset会一直记录并成对存在,我们可以通过以下命令查看replicationIdoffset

> info repliaction

通过redis-cli在master或slave服务器执行该命令会打印类似以下信息(不同服务器数据不同,打印信息不同):

connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=9472,lag=1
master_replid:2cbd65f847c0acd608c69f93010dcaa6dd551cee
master_repl_offset:9472

当master与slave正常连接时,slave使用PSYNC命令向master发送自己记录的旧master的replication id和offset,而master会计算与slave之间的数据偏移量,并将缓冲区中的偏移数量同步到slave,此时master和slave的数据一致。

而如果slave引用的replication太旧了,master与slave之间的数据差异太大,则master与slave之间会使用全量复制的进行数据同步。

3.配置主从复制

Redis的主从配置非常简单,我们可以使用两种方式来配置主从服务器,在这时我们先假设Redismaster服务器地址为192.168.0.101

通过客户端发送同步命令

# 向客户端
slaveof 192.168.1.101 6379

通过slave服务器配置主服务器

在这里slave服务器的redis.conf通过slaveof选项,可以指定master服务器,如下:

slaveof 192.168.1.101 6379

通过上面两种方式的配置,master服务器与slave服务器便已经可以开始进行数据同步了。

master要求验证

上面配置的是master服务器没有设置密码的情况,如果master设置了密码,则可以在连接到slave服务器redis-cli执行下面的命令:

# <password>指代实际的密码
config set masterauth <password>

或者在slave服务器的redis.conf中配置下面的选项:

# <password>指代实际的密码
masterauth <password>

4.注意事项

①避免slave被清空

slave会被清空?slave不用同步了master的数据吗?备份的数据怎么会清空了呢?

当master服务器关闭了持久化时,如果发生故障后自动重启时,由本地没有保存持久化的数据,重启的Redis内存数据为空,而slave会自动同步master的数据,这时候,slave服务器的数据也会被清空。

如何避免slave被清空呢?

如果条件允许(一般都可以的),master服务器还是要开启持久化,这样master故障重启时,可以快速恢复数据,而同步这台master的slave数据也不会被清空。

如果master不能开启持久化,则不应该设置让master发生故障后重启(有些机器会配置自动重启),而是将某个slave服务器升级为master服务器,对外继续提供服务。

②slave默认为只读的

Redis2.6以后,slave只读模式是默认开启的,我们可以通过配置文件中的slave-read-only选项配置是否开启只读模式:

# 默认是yes
slave-read-only yes/no 

或者在客户端中通过config set命令设置是否开启只读模式:

config set slave-read-only no

上面将slave服务器设置为可以写入,但是要注意,如果slave也配置了自己的从服务器(sub-slave),那么sub-slave只会同步从master服务器同步到slave的数据,而并不会同步我们直接写入slave服务器的数据。

主从复制中的key过期问题

我们都知道Redis可以通过设置key的过期时间来限制key的生存时间,Redis处理key过期有惰性删除和定期删除两种机制,而在配置主从复制后,slave服务器就没有权限处理过期的key,这样的话,对于在master上过期的key,在slave服务器就可能被读取,所以master会累积过期的key,积累一定的量之后,发送del命令到slave,删除slave上的key。

如果slave服务器升级为master服务器,则它将开始独立地计算key过期时间,而不需要通过master服务器的帮助。

5.主从复制作用

①保存reids数据副本

当我们只是通过RDBAOFRedis的内存数据持久化毕竟只是在本地,并不能保证绝对的安全,而通过将数据同步slave服务器上,可以保留多一个数据备份,更好地保证数据的安全。

②读写分离

在配置了主从复制之后,如果master服务器的读写压力太大,可以进行读写分离,客户端向master服务器写入数据,在读数据时,则访问slave服务器,从而减轻master服务器的访问压力。

**③高可用性与故障转移
*****服务器的高可用性是指服务器能提供724小时不间断的服务,Redis可以通过Sentinel系统管理多个Redis服务器,当master服务器发生故障时,Sentineal系统会根据一定的规则将某台slave服务器升级为master服务器,继续提供服务,实现故障转移,保证Redis服务不间断。

二、哨兵模式

主从架构虽然可以用于读写分离,减轻单机读的压力,但是当主挂了之后,主从架构无法自动选举出新的master,整个主从无法再提供写能力,所以对于高可用的系统架构模型,主从还远远不够的;必须要有一个高可用的方案,在主服务器挂了之后,在一定条件之内,可以将原先的从服务器选举出一台当做master,使整个redis集群仍然能向外提供读写服务。

1.什么是哨兵

哨兵是 Redis 的一种运行模式,它专注于对 Redis 实例的运行状态进行监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 系统的可用性;总结来说,哨兵具有的功能有如下几个:

  • 监控(monitor):持续监控 master 、slave 是否处于预期工作状态
  • 下线:
    • 客观下线
    • 主观下线
  • 故障转移

2.哨兵功能

2.1监控

默认情况下,哨兵以每秒一次的频率向所有与它创建了命令链接的实例(主服务器,从服务器,其它sentinel服务节点)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线或者存活。

实例对PING命令的回复可分为两种情况:

  • 有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。
  • 无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复。

无效回复的时间配置:sentinel配置文件中的down-after-milliseconds选项指定了sentinel判断实例进入主观下线所需的时间长度,如果一个实例在down-after-milliseconds毫秒内,向Sentinel返回的都是无效回复,则哨兵会将该实例标记为主观下线状态。

2.2下线

①主观下线

哨兵节点利用PING命令来检测与其建立链接的实例是否存活,如果PING命令的回复是无效回复,哨兵节点则会标记该实例为主观下线。

仅仅靠主观下线就判断对应实例是否存活,存在很大的误判性;通过监控一节的介绍可知,对PING命令的回复是无效回复还是有效回复的影响因素有很多,如不同哨兵的负载情况,哨兵节点所在的网络情况,各自的down-after-milliseconds配置。

基于主观下线的话,各个不同的哨兵节点的主观性太强,对于被监控的实例,如果实例的角色(info命令查询)是slave,则可直接通过主观下线来判断该实例是否存活,如果实例的角色是master,则无法通过单个哨兵节点的主观下线行为来判断该实例是否存活,需要结合更多哨兵的监控结果来判断,这样才能降低误判率。

②客观下线

判断master是否下线不能只由一个哨兵说了算;当判断该master主观下线的哨兵节点数量达到quorum(可配置)个时,才能将master标记为客观下线,也就是说这是一个客观存在的事实;

这里的quorum是一个可配置的参数,由哨兵的配置决定;如下:

# sentinel monitor <master-name> <master-host> <master-port> <quorum>

sentinel monitor mymaster 127.0.0.1 6379 2	

这条配置项用于告知哨兵需要监听的主节点:

  • sentinel monitor:代表监控
  • mymaster:代表主节点的名字
  • 127.0.0.1 和6379 分别代表主节点的名字和端口号
  • 2: 表示quorum,代表只有两个或两个以上的哨兵认为主节点不可用的时候,才会把 master 设置为客观下线状态

对不同sentinel来说,它们将master判断为客观下线的条件可能也不同:当一个sentine判断master为客观下线时,其它sentinel可能并不是那么认为。比如,另外一个sentinel的节点quorum配置的值可能是5。

过半机制:当有 N 个哨兵实例时,要有 N/2 + 1 个实例判断 master 为「主观下线」,才能最终判定 Master 为「客观下线」。

2.3自动切换主从

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个sentinel会进行协商,选举出一个领头sentinel,并由领头sentinel对下线主服务器执行故障转移操作。

①选举领头sentinel

在了解选举领头sentinel之前,先记住一个关键词,配置纪元:

配置纪元(configuration epoch) :配置纪元就是一个计数器,每一次的sentinel选举,配置纪元的值都会加1;在一个配置纪元值里面,每一个sentinel有且仅有一次机会,将某个sentineL设置为leader。

为了选举出领头的sentinel,sentinel集群的节点,会互相发送is-master-down-by-addr命令,该命令会带上自己的runId,这表示希望将自己设置成局部领头sentinel,直接上图,如下所示:

选举机制:

  • sentinel设置局部领头sentinel的规则是先到先得:既最先向目标sentinel发送设置要求的源sentinel将成为目标sentinel的局部领头sentinel,而之后接收到的所有设置要求都会被目标sentinel拒绝;
  • 目标sentinel的回复包含leader_runId参数和leader_epoch,分表表示目标sentinel的局部领头运行id和配置纪元;
  • 源sentinel收到命令回复后,会检查leader_epoch的值是否与自己的相同,如果相同的话,那么源sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel;
  • 如果有某个sentinel被半数以上的sentinel设置成了局部领头sentinel,那么这个sentinel成为领头sentinel;在给定时限内,没有一个sentinel被选举为领头sentinel,那么各个sentinel将在一段时间之后再次进行选举,直到选出领头sentinel为止。

②故障转移

选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,故障转移主要包含三个步骤:

  • 选举master:挑选出一个从服务器,并将其转换为主服务器
  • 让已下线主服务器属下的所有从服务器改为复制新的主服务器
  • 如果原下线的主服务器重新上线,使之成为新的master节点的从服务器

1.选出新的主服务器

为了挑选出一个状态良好、数据完整的从服务器,领头的sentinel会通过一定规则来进行刷选(在刷选之前,领头sentinel会将原master的从节点保存为一个列表):

  • 根据节点在线状态:下线的从节点直接从列表剔除。
  • 根据节点网络状态:从列表剔除所有与已下线主服务器连接断开超过down-after-milliseconds*10毫秒的从服务器;如果从库与主库连接断开时间超过阀值,则说明,这个从节点的网络情况并不是很好。

2.通知

当新的主服务器出现之后,领头sentinel下一步要做的就是,让已下线主服务器属下的所有从服务器去复制新的主服务器,这一动作其实就是进行主从关联操作,详细过程,可参考前面的主从一节。

3.哨兵模式搭建

redis-sentinel.conf配置文件

# redis-sentinel.conf

# 使用端口
port 26379
# 指定要监控的Redis服务:<alias> <host> <port> <count>
# <别名> <IP> <端口> <数量,表示count或count以上个哨兵认为主服务器不可用的时候,会进行failover操作>
sentinel monitor mymaster 192.168.1.54 6379 2

# 认证密码
sentinel auth-pass mymaster 123456

# 多少秒内收不到主服务器的ping,就认为它不可用,单位毫秒
sentinel down-after-milliseconds mymaster 5000

# 主节点故障时会重新选举新的主节点,在这个过程中如果新的主节点无法及时接管,这个参数配置的时间会检测到
# 等待一段时间,发现新的主节点仍然不可用则会再次选举
# 集群规模大,节点之间延迟高,或者系统内存在高频写入操作,那么这个值应该尽量小
sentinel failover-timeout mymaster 60000

# 开启DNS解析,这可以解决返回给客户端容器内网IP的问题
SENTINEL resolve-hostnames yes
SENTINEL announce-hostnames yes

有了配置文件就可以写哨兵的启动脚本了:

version: "3"
services:  
  redis-sentinel-master:
    image: redis:7.0.13
    network_mode: mynetwork
    container_name: redis-sentinel-master
    restart: always
    ports:
      - 27000:26379
    volumes:
      - /etc/localtime:/etc/localtime
      - /application/containers/redis/redis-sentinel.conf:/usr/local/redis/sentinel.conf
    command: redis-sentinel /usr/local/redis/sentinel.conf
  redis-sentinel-1:
    image: redis:7.0.13
    network_mode: mynetwork
    container_name: redis-sentinel-1
    restart: always
    ports:
      - 27001:26379
    volumes:
      - /etc/localtime:/etc/localtime
      - /application/containers/redis/redis-sentinel.conf:/usr/local/redis/sentinel.conf
    command: redis-sentinel /usr/local/redis/sentinel.conf
  redis-sentinel-2:
    image: redis:7.0.13
    network_mode: mynetwork
    container_name: redis-sentinel-2
    restart: always
    ports:
      - 27002:26379
    volumes:
      - /etc/localtime:/etc/localtime
      - /application/containers/redis/redis-sentinel.conf:/usr/local/redis/sentinel.conf
    command: redis-sentinel /usr/local/redis/sentinel.conf

**测试:
**以Spring Boot为例,配置文件中这样写:

spring:
  redis:
    password: 123456
    jedis:
      pool:
        max-active: 8 # 连接池最大连接数,负值表示无限制
        max-wait: -1 # 连接池最大阻塞等待时间,负值表示无限制
        max-idle: 500 # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接
    sentinel:
      # 主节点的别名
      master: mymaster
      # sentinel服务的ip和端口
      nodes: mylocalhost:27000,mylocalhost:27001,mylocalhost:27002

启动Spring Boot应用后,可以正常使用Redis,现在把主服务器关掉:

docker stop redis-master

Spring Boot控制台会输出重连日志,然后很快就重新连接上了新的服务器。

Cannot reconnect to [192.168.1.54:7000]: Connection refused: no further information: /192.168.1.54:7000
Reconnecting, last destination was 192.168.1.54:7000
Reconnected to 192.168.1.54:7001

三、分片集群

Redis Cluster是Redis中推荐的分布式集群解决方案。它将数据自动分片到多个节点上,每个节点

负责一部分数据。

Redis Cluster采用主从复制模式来提高可用性。每个分片都有一个主节点和多个从节点。主节点

负责处理写操作,而从节点负责复制主节点的数据并处理读请求。

Redis Cluster能够自动检测节点的故障。当一个主节点失去连接或不可达时,Redis Cluster会尝

试将该节点标记为不可用,并从可用的从节点中提升一个新的主节点。

Redis Cluster是适用于大规模应用的解决方案,它提供了更好的横向扩展和容错能力。它自动管

理数据分片和故障转移,减少了运维的负担。

Cluster模式的特点是数据分片存储在不同的节点上,每个节点都可以单独对外提供读写服务。不

存在单点故障的问题。