美文网首页
关于Redis,你需要知道这些

关于Redis,你需要知道这些

作者: 弹钢琴的崽崽 | 来源:发表于2020-04-27 16:43 被阅读0次

    1. 什么是Redis?

    Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。

    它是一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库

    • 性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS。

    • 单进程单线程,是线程安全的,采用 IO 多路复用机制。

    • 丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。

    • 支持数据持久化。

      可以将内存中数据保存在磁盘中,重启时加载。

    • 主从复制,哨兵,高可用。

    • 可以用作分布式锁。

    • 可以作为消息中间件使用,支持发布订阅。

    Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最的Key-Value DB。 Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

    2. Redis相比memcached有哪些优势?

    1. memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
    2. redis的速度比memcached快很多
    3. redis可以持久化其数据

    3. Redis支持哪几种数据类型?

    首先 Redis 内部使用一个 redisObject 对象来表示所有的keyvalue

    redisObject 最主要的信息如上图所示:type 表示一个 value 对象具体是何种数据类型encoding 是不同数据类型在 Redis 内部的存储方式

    比如:type=string 表示 value 存储的是一个普通字符串,那么 encoding 可以是 raw 或者 int。

    1. String 是 Redis 最基本的类型,可以理解成与 Memcached一模一样的类型,一个 Key 对应一个 ValueValue 不仅是 String,也可以是数字

      String 类型是二进制安全的,意思是 Redis 的 String 类型可以包含任何数据,比如 jpg 图片或者序列化的对象。String 类型的值最大能存储 512M。

    2. Hash是一个键值(key-value)的集合。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适合存储对象。常用命令:hget,hset,hgetall 等。

    3. List 列表是简单的字符串列表按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。

      应用场景:List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 Twitter 的关注列表,粉丝列表都可以用 List 结构来实现。

      数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素。

      实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。

    4. Set 是 String 类型的无序集合。集合是通过 hashtable 实现的。Set 中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion 等。

      应用场景:Redis Set 对外提供的功能和 List 一样是一个列表,特殊之处在于 Set 是自动去重的,而且 Set 提供了判断某个成员是否在一个 Set 集合中。

    5. Zset 和 Set 一样是 String 类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard 等。

      使用场景:Sorted Set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。

      当你需要一个有序的并且不重复的集合列表,那么可以选择 Sorted Set 结构。

      和 Set 相比,Sorted Set关联了一个 Double 类型权重的参数 Score,使得集合中的元素能够按照 Score 进行有序排列,Redis 正是通过分数来为集合中的成员进行从小到大的排序。

      实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。

      而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

    4. Redis主要消耗什么物理资源?

    内存。

    5. Redis有哪几种数据淘汰策略?

    1. noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
    2. allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
    3. volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    4. allkeys-random: 回收随机的键使得新添加的数据有空间存放。
    5. volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
    6. volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

    补充一下:Redis 4.0 加入了 LFU(least frequency use)淘汰策略,包括 volatile-lfu 和 allkeys-lfu,通过统计访问频率,将访问频率最少,即最不经常使用的 KV 淘汰。

    6. Redis 官方为什么不提供 Windows 版本?

    因为目前 Linux 版本已经相当稳定,而且用户量很大,无需开发 windows 版本,反而会带来兼容性等问题。

    7. 一个字符串类型的值能存储最大容量是多少?

    512M

    8. 为什么 Redis 需要把所有数据放到内存中?

    Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。

    所以 redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。

    在内存越来越便宜的今天,redis 将会越来越受欢迎, 如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

    9. 你是怎么使用Redis缓存的?

    结合 Spring Boot 使用的。一般有两种方式,一种是直接通过 RedisTemplate 来使用,另一种是使用 Spring Cache 集成 Redis(也就是注解的方式)。

    Redis缓存

    直接通过 RedisTemplate 来使用,使用 Spring Cache 集成 Redis pom.xml 中加入以下依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    

    spring-boot-starter-data-redis:在 Spring Boot 2.x 以后底层不再使用 Jedis,而是换成了 Lettuce。

    commons-pool2:用作 Redis 连接池,如不引入启动会报错。

    spring-session-data-redis:Spring Session 引入,用作共享 Session。s

    配置文件 application.yml 的配置:

    server:
      port: 8082
      servlet:
        session:
          timeout: 30ms
    spring:
      cache:
        type: redis
      redis:
        host: 127.0.0.1
        port: 6379
        password:
        # redis默认情况下有16个分片,这里配置具体使用的分片,默认为0
        database: 0
        lettuce:
          pool:
            # 连接池最大连接数(使用负数表示没有限制),默认8
            max-active: 100
    

    创建实体类 User.java:

    @Data
    public class User implements Serializable{
        private static final long serialVersionUID = 662692455422902539L;
        private Integer id;
        private String name;
        private Integer age;
    }
    

    RedisTemplate 的使用方式

    默认情况下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定义模板很有必要。

    添加配置类 RedisCacheConfig.java

    @Configuration
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    public class RedisCacheConfig {
    
        @Bean
        public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory connectionFactory) {
    
            RedisTemplate<String, Serializable> template = new RedisTemplate<>();
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setConnectionFactory(connectionFactory);
            return template;
        }
    }
    

    测试类:

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        public static Logger logger = LogManager.getLogger(UserController.class);
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private RedisTemplate<String, Serializable> redisCacheTemplate;
    
        @RequestMapping("/test")
        public void test() {
            redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25));
            User user = (User) redisCacheTemplate.opsForValue().get("userkey");
            logger.info("当前获取对象:{}", user.toString());
        }
    

    然后在浏览器访问,观察后台日志 http://localhost:8082/user/test

    使用 Spring Cache 集成 Redis

    Spring Cache 具备很好的灵活性,不仅能够使用 SPEL(spring expression language)来定义缓存的 Key 和各种 Condition,还提供了开箱即用的缓存临时存储方案,也支持和主流的专业缓存如 EhCache、Redis、Guava 的集成。

    定义接口 UserService.java:

    public interface UserService {
    
        User save(User user);
    
        void delete(int id);
    
        User get(Integer id);
    }
    

    接口实现类 UserServiceImpl.java:

    @Service
    public class UserServiceImpl implements UserService{
    
        public static Logger logger = LogManager.getLogger(UserServiceImpl.class);
    
        private static Map<Integer, User> userMap = new HashMap<>();
        static {
            userMap.put(1, new User(1, "肖战", 25));
            userMap.put(2, new User(2, "王一博", 26));
            userMap.put(3, new User(3, "杨紫", 24));
        }
    
    
        @CachePut(value ="user", key = "#user.id")
        @Override
        public User save(User user) {
            userMap.put(user.getId(), user);
            logger.info("进入save方法,当前存储对象:{}", user.toString());
            return user;
        }
    
        @CacheEvict(value="user", key = "#id")
        @Override
        public void delete(int id) {
            userMap.remove(id);
            logger.info("进入delete方法,删除成功");
        }
    
        @Cacheable(value = "user", key = "#id")
        @Override
        public User get(Integer id) {
            logger.info("进入get方法,当前获取对象:{}", userMap.get(id)==null?null:userMap.get(id).toString());
            return userMap.get(id);
        }
    }
    

    为了方便演示数据库的操作,这里直接定义了一个 Map<Integer,User> userMap。

    这里的核心是三个注解:

    • @Cachable
    • @CachePut
    • @CacheEvict

    测试类:UserController

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        public static Logger logger = LogManager.getLogger(UserController.class);
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private RedisTemplate<String, Serializable> redisCacheTemplate;
    
        @Autowired
        private UserService userService;
    
        @RequestMapping("/test")
        public void test() {
            redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25));
            User user = (User) redisCacheTemplate.opsForValue().get("userkey");
            logger.info("当前获取对象:{}", user.toString());
        }
    
    
        @RequestMapping("/add")
        public void add() {
            User user = userService.save(new User(4, "李现", 30));
            logger.info("添加的用户信息:{}",user.toString());
        }
    
        @RequestMapping("/delete")
        public void delete() {
            userService.delete(4);
        }
    
        @RequestMapping("/get/{id}")
        public void get(@PathVariable("id") String idStr) throws Exception{
            if (StringUtils.isBlank(idStr)) {
                throw new Exception("id为空");
            }
            Integer id = Integer.parseInt(idStr);
            User user = userService.get(id);
            logger.info("获取的用户信息:{}",user.toString());
        }
    }
    

    用缓存要注意,启动类要加上一个注解开启缓存:

    @SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
    @EnableCaching
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    

    缓存注解

    1. @Cacheable

    根据方法的请求参数对其结果进行缓存:

    • Key:缓存的 Key,可以为空,如果指定要按照 SPEL 表达式编写,如果不指定,则按照方法的所有参数进行组合。
    • Value:缓存的名称,必须指定至少一个(如 @Cacheable (value='user')或者 @Cacheable(value={'user1','user2'}))
    • Condition:缓存的条件,可以为空,使用 SPEL 编写,返回 true 或者 false,只有为 true 才进行缓存。

    @2. CachePut

    根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。参数描述见上。

    @3. CacheEvict

    根据条件对缓存进行清空:

    • Key:同上。
    • Value:同上。
    • Condition:同上。
    • allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。
    • beforeInvocation:是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存。缺省情况下,如果方法执行抛出异常,则不会清空缓存。

    10. 你在实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗?

    缓存和数据库数据一致性问题:分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。

    我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。

    合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。

    11. Redis 雪崩了解吗?

    我了解的,目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题。

    举个栗子:如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。

    此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了。

    此时,如果没什么特别的方案来处理,DBA 很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是我理解的缓存雪崩。

    12. Redis雪崩你都是怎么应对的?

    处理缓存雪崩简单,在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。

    setRedis(key, value, time+Math.random()*10000);
    

    如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。

    或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

    • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
    • 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
    • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

    13. 了解缓存穿透和击穿么,可以说说他们跟雪崩的区别吗?

    先说下缓存穿透吧,缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。

    举个栗子:我们数据库的 id 都是从 1 自增的,如果发起 id=-1 的数据或者 id 特别大不存在的数据,这样的不断攻击导致数据库压力很大,严重会击垮数据库。

    至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB。
    而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。

    14. 缓存穿透和击穿分别怎么解决?

    缓存穿透我会在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截。

    Redis 里还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发生。

    它的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在,不存在你 return 就好了,存在你就去查 DB 刷新 KV 再 return。

    缓存击穿的话,设置热点数据永不过期,或者加上互斥锁就搞定了。代码给你准备好了,拿走不谢。

    public static String getData(String key) throws InterruptedException {
        //从Redis查询数据
        String result = getDataByKV(key);
        //参数校验
        if (StringUtils.isBlank(result)) {
            try {
                //获得锁
                if (reenLock.tryLock()) {
                    //去数据库查询
                    result = getDataByDB(key);
                    //校验
                    if (StringUtils.isNotBlank(result)) {
                        //插进缓存
                        setDataToKV(key, result);
                    }
                } else {
                    //睡一会再拿
                    Thread.sleep(100L);
                    result = getData(key);
                }
            } finally {
                //释放锁
                reenLock.unlock();
            }
        }
        return result;
    }
    

    15. Redis 为何这么快

    官方提供的数据可以达到 100000+ 的 QPS(每秒内的查询次数),这个数据不比 Memcached 差!

    16. Redis 这么快,它的“多线程模型”你了解吗?

    您是想问 Redis 这么快,为什么还是单线程的吧。Redis 确实是单进程单线程的模型,因为 Redis 完全是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。

    既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了(毕竟采用多线程会有很多麻烦)。

    17. 你能说说 Redis 是单线程的,为什么还能这么快吗?

    总结一下有如下四点:

    • Redis 完全基于内存,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度是 O(1)。
    • 数据结构简单,对数据操作也简单。
    • 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的 CPU 切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。
    • 使用多路复用 IO 模型,非阻塞 IO。

    18. 你为什么选择 Redis 的缓存方案而不用 Memcached 呢

    原因有如下四点:

    • 存储方式上:Memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
    • 数据支持类型上:Memcache 对数据类型的支持简单,只支持简单的 key-value,,而 Redis 支持五种数据类型。
    • 使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
    • Value 的大小:Redis 可以达到 1GB,而 Memcache 只有 1MB。

    20. 你对 Redis 的持久化机制了解吗?能讲一下吗?

    Redis 为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。

    Redis 的持久化策略有两种:

    • RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。
    • AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。

    当 Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存。

    21. 那你再说下 RDB 是怎么工作的?

    默认 Redis 是会以快照"RDB"的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。

    工作原理简单说一下:当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。

    当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write。

    RDB 的优点是:这种文件非常适合用于备份:比如,你可以在最近的 24 小时内,每小时备份一次,并且在每个月的每一天也备份一个 RDB 文件。

    这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适合灾难恢复。

    RDB 的缺点是:如果你需要尽量避免在服务器故障时丢失数据,那么RDB不合适你。

    22. 那你再说下 AOF 是怎么工作的?

    使用 AOF 做持久化,每一个写命令都通过 write 函数追加到 appendonly.aof 中,配置方式如下:

    将redis.conf中的appendonly改为yes,即开启aof方式的持久化方案。

    appendfsync yes   
    appendfsync always     #每次有数据修改发生时都会写入AOF文件。
    appendfsync everysec   #每秒钟同步一次,该策略为AOF的缺省策略。
    

    AOF 可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。

    使用 AOF 的优点是会让 Redis 变得非常耐久。可以设置不同的 Fsync 策略,AOF的默认策略是每秒钟 Fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。

    缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 Fsync 策略,AOF 的速度可能会慢于 RDB。

    23. 如何选择持久化机制?

    如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以额只使用 RDB 持久。

    AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低Redis的性能,不知道你是否可以接受。

    数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。

    当然了,Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

    24. Redis 单节点存在单点故障问题,为了解决单点问题,一般都需要对 Redis 配置从节点,然后使用哨兵来监听主节点的存活状态,如果主节点挂掉,从节点能继续提供缓存功能,你能说说 Redis 主从复制的过程和原理吗?

    主从配置结合哨兵模式能解决单点故障问题,提高 Redis 可用性。

    从节点仅提供读操作,主节点提供写操作。对于读多写少的状况,可给主节点配置多个从节点,从而提高响应效率。

    关于复制过程,是这样的:

    • 从节点执行 slaveof[masterIP][masterPort],保存主节点信息。
    • 从节点中的定时任务发现主节点信息,建立和主节点的 Socket 连接。
    • 从节点发送 Ping 信号,主节点返回 Pong,两边能互相通信。
    • 连接建立后,主节点将所有数据发送给从节点(数据同步)。
    • 主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

    25. 你能详细说下数据同步的过程吗?

    Redis 2.8 之前使用 sync[runId][offset] 同步命令,Redis 2.8 之后使用 psync[runId][offset] 命令。

    两者不同在于,Sync 命令仅支持全量复制过程,Psync 支持全量和部分复制。

    介绍同步之前,先介绍几个概念:

    • runId:每个 Redis 节点启动都会生成唯一的 uuid,每次 Redis 重启后,runId 都会发生变化。

    • offset:主节点和从节点都各自维护自己的主从复制偏移量 offset,当主节点有写入命令时,offset=offset+命令的字节长度。

      从节点在收到主节点发送的命令后,也会增加自己的 offset,并把自己的 offset 发送给主节点。

      这样,主节点同时保存自己的 offset 和从节点的 offset,通过对比 offset 来判断主从节点数据是否一致。

    • repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是 1MB。

    主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。

    从节点同步主节点数据完成后,主节点将缓冲区的数据继续发送给从节点,用于部分复制。

    主节点响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救。

    上面是 Psync 的执行流程,从节点发送 psync[runId][offset] 命令,主节点有三种响应:

    • FULLRESYNC:第一次连接,进行全量复制
    • CONTINUE:进行部分复制
    • ERR:不支持 psync 命令,进行全量复制

    26. 你能具体说下全量复制和部分复制的过程吗?

    上面是全量复制的流程。主要有以下几步:

    • 从节点发送 psync ? -1 命令(因为第一次发送,不知道主节点的 runId,所以为?,因为是第一次复制,所以 offset=-1)。
    • 主节点发现从节点是第一次复制,返回 FULLRESYNC {runId} {offset},runId 是主节点的 runId,offset 是主节点目前的 offset。
    • 从节点接收主节点信息后,保存到 info 中。
    • 主节点在发送 FULLRESYNC 后,启动 bgsave 命令,生成 RDB 文件(数据持久化)。
    • 主节点发送 RDB 文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。
    • 从节点清理自己的数据库数据。
    • 从节点加载 RDB 文件,将数据保存到自己的数据库中。如果从节点开启了 AOF,从节点会异步重写 AOF 文件。

    关于部分复制有以下几点说明:

    1. 部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync[runId][offset] 命令实现。

      当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,主节点的复制积压缓冲区将这部分数据直接发送给从节点。

      这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。

    2. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内的复制积压缓冲区依然可以保存最近一段时间的写命令数据。

    3. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分复制。

    4. 主节点接收到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点。

      之后根据参数 offset 在复制积压缓冲区中查找,如果 offset 之后的数据存在,则对从节点发送+COUTINUE 命令,表示可以进行部分复制。因为缓冲区大小固定,若发生缓冲溢出,则进行全量复制。

    5. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

    27. 那主从复制会存在哪些问题呢?

    • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。

    • 主节点的写能力受到单机的限制。

    • 主节点的存储能力受到单机的限制。

    • 原生复制的弊端在早期的版本中也会比较突出,比如:Redis 复制中断后,从节点会发起 psync。

      此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。

    28. 如何解决上一题的问题?

    哨兵

    29. 你说下哨兵有哪些功能?

    如图,是 Redis Sentinel(哨兵)的架构图。Redis Sentinel(哨兵)主要功能包括主节点存活检测、主从运行情况检测、自动故障转移、主从切换。

    Redis Sentinel 最小配置是一主一从。Redis 的 Sentinel 系统可以用来管理多个 Redis 服务器。

    该系统可以执行以下四个任务:

    • 监控:不断检查主服务器和从服务器是否正常运行。
    • 通知:当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本向管理员或者其他应用程序发出通知。
    • 自动故障转移:当主节点不能正常工作时,Sentinel 会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
    • 配置提供者:在 Redis Sentinel 模式下,客户端应用在初始化时连接的是 Sentinel 节点集合,从中获取主节点的信息。

    30. 你能说下哨兵的工作原理吗?

    每个 Sentinel 节点都需要定期执行以下任务:每个 Sentinel 以每秒一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 命令。(如上图)

    如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为主观下线。(如上图)

    如果一个主服务器被标记为主观下线,那么正在监视这个服务器的所有 Sentinel 节点,要以每秒一次的频率确认主服务器的确进入了主观下线状态。

    如果一个主服务器被标记为主观下线,并且有足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。

    一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。

    当一个主服务器被标记为客观下线时,Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率,会从 10 秒一次改为每秒一次。

    Sentinel 和其他 Sentinel 协商客观下线的主节点的状态,如果处于 SDOWN 状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。

    当没有足够数量的 Sentinel 同意主服务器下线时,主服务器的客观下线状态就会被移除。

    当主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。

    31. 如何解决 Redis 的并发竞争 Key 问题

    所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

    推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

    基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

    在实践中,当然是从以可靠性为主。所以首推Zookeeper。

    32. 如何保证缓存与数据库双写时的数据一致性?

    一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。

    你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

    一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

    串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

    33. Redis的事务

    概念

    Redis 事务的本质是通过MULTIEXECWATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

    总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

    Redis事务的三个阶段

    1. 事务开始 MULTI
    2. 命令入队
    3. 事务执行 EXEC

    事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

    Redis事务相关命令

    Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

    Redis会将一个事务中的所有命令序列化,然后按顺序执行。

    1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
    2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
    3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
    • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
    • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
    • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
    • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
    • UNWATCH命令可以取消watch对所有key的监控。

    事务管理(ACID)概述

    • 原子性(Atomicity)
      原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
    • 一致性(Consistency)
      事务前后数据的完整性必须保持一致。
    • 隔离性(Isolation)
      多个事务并发执行时,一个事务的执行不应影响其他事务的执行
    • 持久性(Durability)
      持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

    Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

    34. Redis实现分布式锁

    Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

    当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作

    SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

    返回值:设置成功,返回 1 。设置失败,返回 0 。


    使用SETNX完成同步锁的流程及事项如下:

    使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

    为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

    释放锁,使用DEL命令将锁数据删除

    35. 分布式Redis是前期做还是后期规模上来了再做好?为什么?

    既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

    一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

    这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

    36. 什么是 RedLock

    Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

    1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
    2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
    3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务

    参考文章:https://thinkwon.blog.csdn.net/article/details/103522351

    相关文章

      网友评论

          本文标题:关于Redis,你需要知道这些

          本文链接:https://www.haomeiwen.com/subject/rhhuwhtx.html