文章目录



  • 1 Redis集群
  • 1.1 Redis集群概念
  • 1.2 搭建Redis集群
  • 1.3 key->slot
  • 1.4 集群的Jedis开发
  • 2 应用问题解决
  • 2.1 缓存相关问题
  • 2.1.1 缓存穿透
  • 2.1.2 缓存击穿
  • 2.1.3 缓存雪崩
  • 2.2 分布式锁
  • 2.2.1 分布式锁概述
  • 2.2.2 使用Redis实现分布式锁


1 Redis集群

1.1 Redis集群概念

在了解这个概念前,先看几个问题:

1,容量不够,Redis如何扩容?

2,并发写操作,Redis如何分摊?

上述两个问题都可以用搭建集群来解决

Redis集群实现了对Redis的水平扩容,即启动N个Redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N,Redis集群通过分区来提供一定程度的可用性,即使集群中一部分节点失效,集群也能继续处理请求

主从模式和薪火相传模式都会面对主机宕机的情况,导致ip地址发生变化
需要修改配置中master的ip和port,之前通过代理主机来解决

redis 集群 坑 redis 集群问题_lua


代理机需要占用额外的资源,redis3.0中提出了无中心化集群配置

废弃了采用代理机的机制,任何一个RedisServer都可以作为集群入口

请求可以被转发给目标RedisServer

redis 集群 坑 redis 集群问题_lua_02


Redis集群的好处:实现扩容,分摊单机压力,无中心化配置相对简单

Redis集群的弊端:多键操作繁琐,多键Redis事务不支持,Lua脚本不支持

1.2 搭建Redis集群

1,创建6个conf文件

2,每个conf文件配置ip,port,持久化等基础配置及主从哨兵配置 
并增加关于集群的三个配置
cluster-enabled yes 打开集群模式
cluster-config-file xxx.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间 超过该时间(毫秒) 集群自动进行主从切换

3,启动6个Redis服务,使用命令将6个节点组成一个集群

4,redis客户端连接集群时采用 redis-cli -c -p [port]
可以连接到集群中目的端口的RedisServer
此时完成无中心化集群配置

redis 集群 坑 redis 集群问题_redis 集群 坑_03

1.3 key->slot

一个Redis集群包含16384个slot,数据库中的每个键都属于这些slot中的一个
集群使用CRC16(key)%16384来计算key对应哪个slot类似hashmap,集群中的每个节点处理一部分slot

类似于一致性hash的环:

redis 集群 坑 redis 集群问题_Redis_04


先计算k1键对应的slot,将对k1的读写操作交给对应的RedisServer

由于集群中的RedisServer互相通信,通过无中心化配置,请求可以被转发到目标RedisServer注意,在集群中是不能直接使用mset等命令进行写操作,因为命令中伴随不止一个key

集群没法计算出对应的slot

redis 集群 坑 redis 集群问题_数据库_05


为此可以采用分组的方式操作,如定义多个key属于同一组,即存放在同一台机器中

redis 集群 坑 redis 集群问题_redis 集群 坑_06

1.4 集群的Jedis开发

搭建好Redis集群后,创建JedisCluster实例以操作集群

public static void main(String[] args) {
        // 1,创建JedisCluster对象 以操作集群
        // 这里选择的port并不重要 redis集群是无中心化配置的
        JedisCluster jedisCluster = new JedisCluster(new HostAndPort("127.0.0.1", 6379));
        
        // 2,调用api 读写redis集群
        jedisCluster.set("k1", "v1");
        System.out.println(jedisCluster.get("k1");

        jedisCluster.close();
    }

2 应用问题解决

2.1 缓存相关问题

2.1.1 缓存穿透

缓存穿透:
某刻有大量请求访问应用服务器时
应用服务器首先在Redis中查询是否存在缓存
但此刻Redis中不存在缓存的数据 此时应用服务器只能到DB中直接去查询
为此造成某刻DB承受太多读写请求 导致DB瘫痪 这就是缓存穿透

redis 集群 坑 redis 集群问题_lua_07


此种现象出现的原因是Redis中无缓存数据+某刻出现大量非正常url访问

不部分是网站遭受了攻击导致,其解决方案有:

1,对空值做缓存,如果查询返回的数据为空,不管该数据存不存在
都把这个空结果进行缓存,并设置一个较短的TTL 只能作为简单的应急方案

2,设置白名单,使用bitmap定义一个有效访问名单
访问者id作为bitmap的偏移量,每次将访问者id在名单中进行判断
如果不在白名单中进行拦截 不允许访问
但每次现在白名单中判断是否为有效id 又降低了系统效率

3,采用布隆过滤器,布隆过滤器可看作是优化的bitmap
可以用于检索一个元素是否在bitmap中 优点是时空间效率更高 但缺点是可能出现误判

4,进行实时监控,当发现Redis命中率某刻开始急速降低
需要排查访问对象和访问的数据,让运维设置黑名单

2.1.2 缓存击穿

缓存击穿:
某刻有大量请求进入应用服务器 应用服务器去Redis中获取数据
发现key虽然存在,但是已经过期,不得不直接访问DB 并将最新数据从DB中加载到Redis缓存
但此时大量的请求突然涌入可能导致DB崩溃

redis 集群 坑 redis 集群问题_redis 集群 坑_08


此种现象出现的原因是某刻出现大量对某个key请求+这个key在Redis中过期

如秒杀的商品key在Redis中过期,为此解决的方案有:

1,根据业务预先设置热门数据,在Redis高峰访问前
将热门数据提前存在Redis中,并加长这些key的过期时间
如有紧急新闻,各媒体平台预先设置这些key 以应对热搜

2,实时调整,监控哪些数据热门 并实时调整key的过期时长

除此外还可以使用锁来处理,但用到锁难免降低效率:

redis 集群 坑 redis 集群问题_redis 集群 坑_09

2.1.3 缓存雪崩

缓存雪崩:
大量的key存在,但是这些key对应的数据都已经过期
此时如果涌入大量请求,发现缓存中的数据已经过期 就会直接访问DB
由此可能造成DB的崩溃

缓存击穿和缓存雪崩的区别在于,击穿是1个key失效,雪崩是多个key失效

该现象出现的原因在于某刻涌入大量请求+大量key集中过期
为此可以使用以下方案避免:

1,构建多级缓存架构
通过nginx缓存+redis缓存+其它缓存ehcache等

2,使用锁或队列
用加锁或队列的方式保证不会有大量线程对DB进行集中读写
但此种方式不适用高并发场景

3,设置过期标志更新缓存
记录缓存数据是否过期,如果过期触发一个线程去DB更新数据

4,将缓存失效时间分开
如对某一批key原先的失效时间设置一个随机值
这些key在1-5min随机失效,就不会出现大量key失效的场景

2.2 分布式锁

2.2.1 分布式锁概述

随着业务发展需要,原单机部署的系统被演化为分布式集群系统后
由于分布式系统多线程,多进程分布在不同机器上,这将使原先单机部署情况下得并发锁控制策略失效
单纯的JavaAPI并不能提供分布式锁,为了解决这个问题就需要一种跨JVM的互斥机制来控制对共享资源的访问

看看如下的一个场景:
AB两用户正在参与秒杀,每个人想秒杀10件商品,此时库存12件

订单系统是单机情况下,通过加锁来保证共享资源安全:

redis 集群 坑 redis 集群问题_Redis_10


订单系统是分布式的情况下,没有分布式锁会出现如下的情况:

redis 集群 坑 redis 集群问题_redis 集群 坑_11


为了保证共享数据的安全,采用分布式锁保证某刻集群中只有一台机器能完成对共享数据的读写
其他拿不到分布式锁的机器只能等待锁被释放,才能去操作共享数据
上述例子,采用分布式锁后:

redis 集群 坑 redis 集群问题_lua_12


分布式锁的主流实现方案:

1,基于数据库实现分布式锁
2,基于缓存实现(Redis等)
3,基于Zookeeper等

每种分布式锁都有其优缺点,如Redis的分布式锁性能最高,ZK的分布式锁可靠性最高

2.2.2 使用Redis实现分布式锁

先来看一个命令setnx,即key不存在进行创建key并赋值,key已存在不可以再setnx

setnx k1 20 即添加k1设置值为20

redis 集群 坑 redis 集群问题_lua_13


但这样的锁过于简陋,可能一直不释放导致饿死也可能被其他机器将该锁误删

为了解决饿死的问题,可以设置一个过期时间:

set k1 10 nx ex 10

该命令等价于 setnx k1 10 + expire k1 10

释放锁可以先简单用del命令来实现,以一段java代码简单模拟:

public static void testLock() {
        
        // 试图加锁 
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.SECONDS);
        
        if(lock) {
            // 加锁成功 对num的值进行修改
            Object value = redisTemplate.opsValue().get("num");
            if(StringUtils.isEmpty(value)) {
                return;
            }
            
            int num = Integer.parseInt(value + "");
            redisTemplate.opsForValue().set("num", ++num);
            redisTemplate.delete("lock");
        } else {
            // 加锁失败 sleep一段时间后再尝试获取锁
            try {
                Thread.sleep(1000);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        
    }

但仍有缺陷,未考虑到锁被误删(错误释放)的情况
考虑这样的情况,A和B机器要修改Redis服务器中的某个数据,A先到并成功加锁,锁的过期时间设置为10s
但此时A机器遇到意外卡顿等情况超过了10s但还没有成功修改,此时B已经成功加锁
这时A和B又开始一起操作共享数据,A先做完操作去释放锁,此时释放的是B加的锁
这样不仅B释放错误,还可能又导致数据混乱的情况
为此,对其可以采用UUID防止误删,通过uuid标识不同的机器
释放锁时首先判断当前uuid和要释放的锁的uuid是否相同

public static void testLock() {
        
        String uuid = UUID.randomUUID().toString();

        // 试图加锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);

        if(lock) {
            // 加锁成功 对num的值进行修改
            Object value = redisTemplate.opsValue().get("num");
            if(StringUtils.isEmpty(value)) {
                return;
            }

            int num = Integer.parseInt(value + "");
            redisTemplate.opsForValue().set("num", ++num);
            
            // 释放锁
            if(uuid.equals(redisTemplate.opsForValue().get("lock"))) {
                redisTemplate.delete("lock");
            }
        } else {
            // 加锁失败 sleep一段时间后再尝试获取锁
            try {
                Thread.sleep(1000);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }

但上述释放锁的过程又少了原子性,同样AB两台机器访问共享资源
A先加锁,执行完了操作,判断了uuid相同,准备释放锁时,此时锁过期,自动释放
此时B加锁成功,执行操作,但A又释放了锁,释放的正是B的锁,又会导致互相干扰
为了保证Redis中的原子性,可以采用Lua脚本