redis学习

1、NoSql数据库简介

1.1、讨论session问题

问题描述:客户端发送请求到服务器,但是该服务器用nginx代理,采取分布式服务,第一次请求发送到第一个服务器,此时session存在第一个服务器中,第二次请求则可能会被nginx转发到第二个服务器中,此时第二台服务器就没有session了。如何解决这个问题?

解决方法:(1)、存储到cookie

好处:每次发送请求都会带着cookie,能保证session共享

缺点:存储到客户端,安全性较差

(2)、session复制 :在第一台服务器登陆后,复制session到其他服务器中

缺点:空间浪费,数据冗余

(3)、Nosql数据库:当用户第一次登陆后,将session写入到Nosql数据库中,第二次登陆,第二个服务器也去Nosql中读取Session

好处:不需要进行io操作,完全存在内存中,速度快,数据结构简单

1.2、Nosql数据库概述

Nosql = Not only SQL,意思为“不仅仅是SQL”,泛指非关系型数据库。Nosql不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。

(1)、不遵循SQL标准。

(2)、不支持ACID。

(3)、远超于SQL的性能

1.3、Nosql的使用场景

(1)、对数据高并发的读写

(2)、海量数据的读写

(3)、对数据高可扩展性的

1.4、Nosql的不适用场景

(1)、需要事务支持

(2)、基于sql的结构化查询存储,处理复杂的关系,需要即席查询

1.5、常见Nosql

(1)、Memcache

很早出现的Nosql数据库

数据都在内存中,一般不持久化

支持简单的key-value模式,支持类型单一

一般是作为缓存数据库辅助持久化的数据库

(2)、Redis

几乎覆盖了Memcache的绝大部分功能

支持持久化(就是把数据存在硬盘中,下次开启还存在着,单单存在内存中的话,可能一不小心就没了)

除了支持简单的key-value模式,还支持多种数据结构的存储,比如list、set、hash、zset等

一般是作为缓存数据库的辅助持久化的数据库

(3)、MongoDb

高性能、开源、模式自由的文档型数据库(Json格式)

数据都存在内存中,如果内存不足,把不常用的数据保存到硬盘中

虽然是key-value模式

2、Redis6的概述与安装

Redis是一个开源的key-value系统

和Memcached类似,他支持更多的value类型,包括String,list(链表),set(集合),zset(sorted set有序集合)和hash

这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

在此基础上,Redis支持各种不同方式的排序

和Memcached一样,为了保证效率,数据都是缓存在内存中

区别是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。

并且在此基础上实现了主从同步。

2.1、配合关系型数据库做高速缓存

高频词,热门访问的数据,降低数据库IO

分布式架构,做session共享

多样的数据结果存储持久化数据

(1)最新N个数据 — 通过List实现自然时间排序的数据

(2)排行榜,TopN — 利用zset有序集合

(3)时效性的数据,如手机验证码 — Expier过期

(4)计数器,秒杀 — 原子性,自增方法INCR、DECR

(5)去除大量数据中的重复数据 — 利用Set集合

(6)构建队列 — 利用List集合

(7)发布订阅消息系统 — pub/sub模式

2.2、redis安装

安装步骤参考:ubuntu 18.04 安装 Redis - 别再闹了 - 博客园 (cnblogs.com)

使用ubnutn安装redis时会出现两个错误

解决update错误:https://www.cnblogs.com/ceason/p/13748404.html

2.2.1、安装时报错:“subprocess installed post-installation script returned error exit status 10”报错

解决方法:(17条消息) 【已解决】“subprocess installed post-installation script returned error exit status 10”报错_深视的博客-CSDN博客

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQUOBAzN-1660038334037)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220711235239102.png)]

2.2.2、设置redis开机自启时报错:Job for redis-server.service failed because a timeout was exceeded. See “systemctl status redis-server.service” and “journalctl -xe” for details.

解决方案:进入 /etc/redis/redis.conf 找到 bind127.0.0.1 ::1 将其修改为 bind 0.0.0.0

redis启动不了 redis-server.service failed because a timeout was exceeded - Magento2专业中文社区 (mallol.cn)

2.3、Redis相关知识

1、端口6379从何而来

很扯淡。。。一个女明星叫 Alessia Merz 然后 Merz在九键键盘上的数字是6379。。。

2、默认16个数据库,类似数组下标从0开始,初始默认用0号库。

3、使用select 8 来选择数据库

4、统一密码管理,所有库同样密码

5、dbsize查看当前数据库的key数量

6、flushdb 清空当前库

7、flushall 通杀全部库

8、Redis是单线程+多路io复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(如使用线程池)

Memcached使用:多线程+锁

什么叫串行:我的理解就是一个一个操作,按照顺序进行操作。

3、常用五大数据类型

3.1、字符串(String)

3.1.1、常用命令

set 添加键值对

get 查询对应的键值对

append 将给定的value追加到原值的末尾

strlen 获得值长度

setnx 只有key不存在时,设置key的值

incr 将key中储存的数字值增1,只能对数字值进行操作,如果为空,新增值为1

decr将key中储存的数字值减1

incr 步长 将key中储存的数字值增加步长,decr同理

incr和decr的操作是原子性的,即一个失败则全部失败

mset 设置多个key-value

mget 查询多个key-value

msetnx 设置多个key-value且只有当key存在时才会设置。它是原子性的,即一个失败就全部失败

get range <起始位置><结束位置> 截取Key对应的value值

set range <起始位置> 用value覆盖key所储存的字符串,从起始位置开始

setex <过期时间>设置键值得同时,设置过期时间,单位秒

getset以新换旧,设置了新值同时获得旧值

3.1.2、数据结构

String的数据结构为简单动态字符串。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

String扩容

字符串实际分配的空间capacity一般要高于实际字符串长度len,当字符串长度小于1M时,扩容都是加倍现有空间,就是double,如果超过1M,扩容时只会多扩1M的空间,需要注意的是字符串最大长度为512M

3.2、列表(List)

单键多值

3.2.1、常用命令

lpush/rpush

3.2.2、数据结构

List的数据结构为快速链表quickList

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表

当数据量比较多的时候才会改成quickList

因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next

Redis将链表和zipList结合起来组成了quickList。也就是将多个zipList使用双向指针传起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3.3、集合(set)

Redis set对外提供的功能与List类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择。

Redis set时String类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)

3.3.1、数据结构

Set数据结构时dict字典,字典是用哈希表实现的。

Java中的HashSet的内部实现使用的是HashMap,只不过所有的value都指向通过一个对象。

Redis的set结构也是一样的,她的内部也使用hash结构,所有的value都指向同一个內部值。

3.4、哈希(Hash)

Redis hash是一个键值对集合。

Redis hash是一个String类型的field和value的映射表,hash特别适合用于存储对象。类似于Java里的Map

用户ID为查找到的key,存储的value

3.4.1、数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度短且个数较少时,使用zip-list,否则使用hashtable。

3.5、有序集合(Zset)

Redis有序集合Zset与普通集合set非常相似。是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(Score),这个score被用来按照从最低分到最高分的方式排序集合中的成员。集合中的成员是唯一的,但是评分可以是重复的。

3.5.1、数据结构

zset是redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map,可以给每个元素的value赋予一个权重score,另一方面它又类似于TreeSet,内部元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相对应的score值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XeZDrLJ-1660038334038)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220715150622097.png)]

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGwcrXpk-1660038334039)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220715150637863.png)]

3.6Reis键(Key)

3.6.1、操作命令:

keys *查看当前库所有的key(匹配:key *1)

exists key判断某一个key是否存在

type key查看你的key是什么类型

del key 删除指定的key数据

unlink key根据value选择非阻塞删除 仅将keys从keyspace元数据中删除,真正的删除会在后续一部操作

expire key 10 为给定的key设置国企时间

ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期

4、Redis6配置文件详解

5、Redis6的发布与订阅

5.1、常用命令

1、订阅频道 subscribe channell

2、发布消息 pulish channell hello 返回的是订阅者数量

6、Redis6新数据类型

6.1、Bitmaps 位操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTKiyawv-1660038334040)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220716180516893.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hvBopCs-1660038334040)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220716180553889.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FArMD6D-1660038334043)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220716180605478.png)]

6.2、HyperLogLog

计算基数 就是计算不重复元素的数量。适用于计算浏览量

6.3、Geospatial

地理信息的缩写,也就是2维坐标,在地图上就是经纬度,redis基于该类型提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。常用命令就是用来存储经纬度地址还有计算距离,获取半径之内所有地点等功能。

7、Jedis操作Redis6

就是用Java操作Redis

创建一个Maven工程,并在pom.xml引入jedis依赖


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>3.2.0version>
dependency>

创建一个Jedis测试类

//创建Jdis对象
Jedis jedis = new Jedis("xxxx",6379);

//测试
String value  = jedis.ping();
System.out.println(value);

此时安装我们redis的服务器需开启6379端口

具体命令:ufw allow 6379 提示权限不足就使用sudo ufw allow 6379 如果是购买的云主机还需要在安全组添加6379端口

修改服务器中redis配置文件,redis.conf中注释掉bind 127.0.0.1然后protected-mode no

测试相关数据类型

操作key

package test;

import org.junit.Test;
import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: cyijie
 * @Date: 2022/07/16/19:52
 * @Description:
 */
public class JdeisDemo {

    @Test
        //操作key
    public void keyDemo(){
        //创建Jedis对象
        Jedis jedis = new Jedis("xxxxx",6379);

        //添加
        jedis.set("k1","v1");

        //设置多个key-value
        jedis.mset("k2","v2","k3","k4");

        //获取
        String v1 = jedis.get("k1");
        System.out.println(v1);

        Set<String> keys = jedis.keys("*");
        System.out.println(jedis.exists("k1"));
        for(String key:keys){
            System.out.println(key);
        }
    }

    @Test
    public void listDemo(){
        Jedis jedis = new Jedis("xxxxx",6379);
        jedis.lpush("key1","lucy","mike","jack");
        List<String> values = jedis.lrange("key1",0,-1);
        System.out.println(values);
    }

    @Test
    public void setDemo(){
        Jedis jedis = new Jedis("xxxxx",6379);
        jedis.sadd("name","lucy","mike","jack");
        Set<String> smembers = jedis.smembers("name");
        for(String order:smembers){
            System.out.println(order);
        }
    }

    @Test
    public void hashDemo(){
        Jedis jedis = new Jedis("xxxxx",6379);
        jedis.hset("users","age","20");
        String age = jedis.hget("users","age");
        System.out.println(age);
    }

    @Test
    public void zsetDemo(){
        Jedis jedis = new Jedis("xxxxx",6379);

        jedis.zadd("china",100d,"shanghai");
        jedis.zadd("china",99d,"fuzhou");
        jedis.zadd("china",51d,"xiamen");

        Set<String> china = jedis.zrange("china",0,-1);
        System.out.println(china);
    }
}

模拟验证码发送

1、生成随机6位数字验证码

2、验证码在2分钟内有效 把验证码放到redis中,设置过期时间120s

3、验证码是否一致 从redis获取验证码和输入的验证码进行比较

4、每个手机每天只能输入3次 incr每次发送之后+1 大于2的时候,提交不能发送

package test;

import org.junit.Test;
import redis.clients.jedis.Jedis;

import java.util.Random;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: cyijie
 * @Date: 2022/07/18/10:11
 * @Description:模拟验证码
 */
public class PhoneCodeDemo {

    @Test
    public void testMain(){
        String phone = "18159320225";
        String code = getCode();
        //模拟验证码发送
        verifyCode(phone,code);

        //验证
//        getRedisCode(phone,"878132");
    }

    //1、生成六位数字验证码
    public static  String getCode(){
        Random random = new Random();
        StringBuilder code  = new StringBuilder();
        for(int i = 0 ;i<6 ;i++){
            code.append(random.nextInt(10));
        }
        return code.toString();
    }

    //2、验证码2分钟内有效,验证码放到redis中
    public static void verifyCode(String phone,String code){
        Jedis jedis = new Jedis("xxxxxx",6379);

        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode" + phone +":count";
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code";

        //每个手机每天只能发三次
        String count = jedis.get(countKey);
        if(count == null){
            //没有发送,第一次发送
            //设置发送次数是1
            jedis.setex(countKey,24*60*60,"1");
        } else if(Integer.parseInt(count)<=2){
            //发送次数+1
            jedis.incr(countKey);
        } else if(Integer.parseInt(count)>2){
            System.out.println("今天已经发送三次,不能再发送了");
            jedis.close();
        }

        //设置验证码过期时间
        jedis.setex(codeKey,120,code);
        jedis.close();
    }

    //3、验证码校验
    public static void getRedisCode (String phone,String code){
        //从redis获取验证码
        Jedis jedis = new Jedis("xxxx",6379);
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code" ;
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)){
            System.out.println("验证成功");
        }else {
            System.out.println("验证码不一致");
        }
        jedis.close();
    }
}

8、Redis6与SpringBoot整合

在pom.xml引入redis依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>

在application.properties中配置redis相关内容

#Redis服务器地址
spring.redis.host=xxxxxx
#Redis服务连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database=0
#连接超时时间
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0

创建一个config包,在里面新建一个redis配置类,命名为RedisConfig

@EnableCaching
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存异常转换的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //配置序列化(解决乱码问题),过期时间600秒
        RedisCacheConfiguration config  = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

新建一个redis测试类

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis(){
        //设置值到redis
        redisTemplate.opsForValue().set("name","jack");
        //从redis获取值
        String name = (String) redisTemplate.opsForValue().get("name");
        return name;
    }
}

这边运行这个springboot项目时出现了一个错误:An attempt was made to call a method that does not existThe attempt was made from following location

出现这个原因是由于子依赖与父依赖版本不一致导致,在这里我是将commons-pool2这个依赖的标签删除就成功解决问题了

当这个问题解决后又出现了一个端口号占用问题,因为此时我本地的8080端口已经被占用,所以这边我要修改springboot的版本号

在application.properties中添加

#修改默认端口
server.port=8081

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDnQUD0w-1660038334045)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220718144448053.png)]

此时就可以重新启动了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2vsUUuA-1660038334048)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220718144525397.png)]

页面访问正常

9、Redis6的事务操作

9.1、概述

Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis 事务的主要作用就是串联多个命令防止别的命令插队。

Multi*、Exec、*Discard

Multi相当于开启事务

把命令依次放到队列中

Exec相当于提交事务

Discard相当于回滚事务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLUeE22Q-1660038334049)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220718150652404.png)]

9.2、事务的错误处理

1、组队中某个命令出现了报告错误,执行时整个所有的队列都会被取消

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3JyjUhIP-1660038334051)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220718150859240.png)]

2、如果执行阶段某个命令报错,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

9.3、事务冲突问题

例子:一个请求想给金额减8000,一个请求想给金额减5000,一个请求想给金额减1000

悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前上锁。

悲观锁在redis中的实际操作:

打开两个redis服务终端,先添加一个key,保证两个终端都有一个共同的key,然后分别在两个终端输入watch命令,watch命令就是用来监视key,如果在事务执行之前,这些key被其他命令所改动,那么事务将被打断。然后在其中一个终端对这个key进行操作,并执行事务,此时可以看到事务执行成功。这时候在另一个终端也进行相应的操作,执行事务之后会提示nil,该终端的事务就是不被执行了。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁使用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKza29YY-1660038334054)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220718160713011.png)]

9.4、Redis的事务三特性

1、单独的隔离操作

事务所有命令都会被序列化,按顺序地执行。事务在执行的过程中,不会被其他客户端发来的命令请求打断。

2、没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

3、不保证原子性

事务中如果有一条命令执行失败,其后的命令仍会被执行,没有回滚。

9.5、秒杀案例:

1、代码实现

2、秒杀并发模拟

使用ab模拟测试

ubnutu安装ab模拟测试(3条消息) ubuntu下 安装ab(apache bench)_FenG·的博客-CSDN博客_ubuntu 安装ab

解决超时超卖问题:

(1)、连接超时,通过连接池解决

节省每次连接redis服务带来的消耗,把连接好的实例反复利用。通过参数管理连接行为。

(2)、解决超卖问题,进行事务操作,对数量进行监听

(3)、库存遗留问题

乐观锁造成的库存遗留问题

10、Redis6持久化之RDB

10.1、RDB(Redis DateBase)是什么?

在指定的时间间隔将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件读到内存中。

10.2、RDB备份是如何执行的?

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

Fork的作用是复制一个与当前线程一样的进程。新进程所有的数据(变量、环境变量、程序计数器等)数值和原进程一致,但是是一个全新的进程,并作为原进程的子进程。在linux系统中,fork()会产生一个和父进程相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,Linux中引入了“写时复制技术”

10.3、dump.rdb文件

在redis.conf中配置文件名称,默认为dump.rdb

10.4、RDB的优势

适合大规模的数据恢复

对数据完整性和一致性要求不高更适合使用

节省磁盘空间

恢复速度快

10.5、劣势

1、Fork的时候,内存的数据被克隆了一份,大致2倍的膨胀性需要考虑

2、虽然Redis在Fork时使用了写时拷贝技术,但是如果数据庞大的话,还是比较消耗性能的。

3、在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

11、Redis6持久化之AOF

11.1、AOF(Append Only File)是什么

AOF以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只允许追加文件不可以修改文件,Redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

11.2、AOF持久化流程

(1)客户端的请求写命令会被append追加到AOF缓冲区中。

(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中

(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量

(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的

11.3、AOF默认不开启

可以在redis.conf中配置文件名称,默认为appendonly.aof,AOF文件的保存路径和RDB的路径一致

11.4、AOF和RDB同时开启,redis听谁的

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

11.5、AOF启动/修复/恢复

AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,系统启动即加载。

正常恢复

修改默认的appendonly no 改为 yes

将有数据的aof文件复制一份到对应目录

恢复:重启redis然后重新加载

异常恢复

修改默认的appendonly no 改为 yes

如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复

备份被写坏的AOF文件

恢复:重启redis然后重新加载

11.6、AOF同步频率设置

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

appendfsync everysec

每秒同步。每秒记入日志一次,如果宕机,本秒的数据可能丢失

appendfsync no

redis不主动进行同步,把同步时机交给操作系统

11.7、Rewrite压缩

AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设置的阈值时,Redis就会启动AOF的内容压缩,只保留可以恢复数据的最下指令集,可以使用命令bgrewriteaof

重写原理?如何实现重写?

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0后版本的重写,就是把rdb的快照,以二进制的形式附在新的AOF头部,作为已有的历史数据,替换到原来的流水账操作

no-appendfsync-on-rewrite

什么时候触发重写?

Redis会记录上次重写的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M触发

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB.达到这个值开始重写。

重写流程

(1)bgrewrite触发重写,判断是否当前有bgsave或bgrewrite在运行,如果有,则等待命令结束后再继续执行。

(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞

(3)子进程便利redis内存中数据临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_write_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失

(4)子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。主进程把aof_rewrite_buf中的数据写入到新的AOF文件

(5)使用新的AOF文件覆盖新的AOF文件,完成AOF重写

11.8、优势

备份机制更加稳健,丢失数据概率更低

可读的日志文本,通过操作AOF文件,可以处理误操作

11.9、劣势

比起RDB占用更多的磁盘空间

恢复备份速度较慢

每次读写都同步的话,有一定的性能压力

存在个别BUG,造成不能恢复

11.10、AOF和RDB总结

官方推荐两个都启用

如果对数据不敏感,可以选单独用RDB

不建议单独用AOF,因为可能存在Bug

如果单纯做纯内存缓存,可以都不用

12、Redis6的主从复制

12.1、什么是主从复制?

主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slaver以读为主

12.2、能干吗?

(1)读写分离,性能扩展。主机做写操作,从机做读操作

(2)容灾快速恢复,当某一台从机挂掉之后,能快速切换到其他从机,继续提供服务

12.3、如何搭建主从复制

前提是需先将redis.conf中的appendonly yes改为no 再进行复制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k3OWEXxe-1660038334055)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220728001333703.png)]

以上操作是一次性操作,没办法实现永久主从配置,需在配置文件添加信息才能实现永久主从

12.4、主从复制原理(简单描述,详细描述得百度)

(1)当从服务器脸上我们的主服务器之后,从服务器会向主服务器发送进行数据同步消息

(2)当主服务器接受到从服务器发送过来的同步消息,把主服务器数据进行持久化,rdb文件,把rdb文件发送到从服务器,从服务器拿到rdb进行读取

(3)每次主服务器进行写操作之后,都会和从服务器进行数据同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvhgR7rm-1660038334056)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220728004057280.png)]

12.5、常用三招

(1)一主二仆

当我们其中一台从服务器宕机后,此时的主服务器还在进行写操作,此时我们的从服务器是没有挂掉之后的数据的。但是当我们从服务器重启之后,会自动复制我们主服务器里的所有数据,所以不会产生数据丢失。

(2)薪火相传

当我们的从服务器增多之后,我们的主服务器往从服务器同步数据的速度会变得越来越慢,为了解决这个问题,我们可以选择一台或多台的从服务器成为其余从服务器的主服务器。此时我们的主服务器往从服务器同步数据不再是一次像多个从服务器同步了,而是先同步给几台从服务器,再由这几台从服务器同步给我们剩下的从服务器。该方法有一个弊端就是,当我们的主要从服务器宕机之后,我们的主服务器没办法向他们的下下级从服务器同步数据。

(3)反客为主

就是当我们的主服务器宕机且启动不了时,我们可以选择一台从服务器成为我们新的主服务器,需要在控制台输入一段代码(slaveof no one)。这里实现的是手动版的反客为主,自动版的反客为主需要哨兵模式。

12.6、哨兵模式

(1)反客为主的自动版,能够监控主机是否控制,如果故障了根据投票数自动将从库转换为主库

(2)如何使用?

1)调整为一主二仆模式,就是为主服务器配置从服务器咯

2)新建sentinel.conf文件,名字绝对不能错

3)配置哨兵,填写内容 sentinel monitor mymaster 127.0.0.1 6379 1。其中mymaster为监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量。

4)启动哨兵

redis-sentinel /sentinel.conf(redis6.0)

redis-server myredis/sentinel.conf --sentinel(redis4.0)

5)当主机挂掉,从机选举中产生新的主机。大概10秒左右可以看到哨兵窗口的日志,切换了新的主机。从机会根据配置文件中的slave-priority来选择谁当主机。原主机重启后会变成从机。

6)由于所有的写操作都是再主机中操作,然后同步更新到slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙,延迟问题会更加的严重,从机数量的增加也会使这个问题更加严重。

(3)故障恢复

从下线的主服务的所有从服务器中挑选一个从服务,将其转成主服务。选择条件依次为:1、选择优先级靠前的,优先级就是在配置文件中声名的slave-priority(redis6.0之后似乎就改变了,反正肯定带priority)2、选择偏移量大的,偏移量是指获得原主机数据最全的3、选择runid最小的从服务器,每个redis实例启动后都会随机生成一个40位的runid

挑选出新的主服务之后sentinel向原主服务器的从服务器发送指令,复制新的主服务器,当已经下线的主服务器重新上线时,sentinel会向其发送指令,让其成为新主服务器的从服务器。

13、Redis6集群

13.1、思考问题?

容量不够时,redis如何进行扩容?

并发写操作,redis如何分摊?

另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址,端口等信息

之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。

代理主机:需要一台代理主机来处理转发请求到各个服务器。这种配置对服务器数量要求比较大

无中心化集群配置:任何一台服务器都可以作为集群的路口。每个服务器都可以转发请求。

13.2、什么是集群?

Redis集群实现了对redis的水平扩容,即启动N个节点,将数据库分布存储再这N个节点中,每个节点存储总数据的1/N

Redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求

13.3、搭建一个redis集群(经济有限,在一台服务器里模拟搭建集群)

(1)、删除aof、rdb持久化文件。不删掉的话会对集群有干扰。

(2)、修改相应的配置文件即 Redis.conf

​ ##都要设置文件存放路径,不然找不到,就像这个nodes-xxxx.conf,一开始以为是未生成,没想到是在根目录生成了,大坑!##

​ 1)、reids cluster配置修改

​ cluster-enabled yes /打开集群模式/

​ cluster-config-file myredis/nodes-6379.conf /设置节点配置文件名/

​ cluster-node-timeout 1500 /设定节点失联时间,超过该时间,集群自动进行主从切换/

​ 2)、制作六个实例,端口号分别为:6379、6380、6381、6389、6390、6391

(3)、启动六个实例

(关闭redis自启动服务命令:/etc/init.d/redis-server stop)

(4)、安装ruby(redis4.0需要安装,redis6.0似乎不用)总的来说就是找到redis的安装目录,然后再安装目录的src目录下运行一下命令

./redis-trib.rb create --replicas 1 xxxx:6379 xxxxx:6380 xxxxxx:6381 xxxx:6389 xxxxx:6390 xxxxx:6391(redis4.0.9)

redis-cli --cluster --replicas 1 xxxxx:6379 xxxxxxx:6380 xxxxxxx:6381 xxxxxx:6389 xxxxxxxx:6390 xxxxxxx:6391(redis6.2.1)

redis-cli --cluster create xxxxxxx:6379 xxxxxx:6380 xxxxxxx:6381 xxxxxxx:6389 xxxxxx:6390 xxxxxxx:6391 --cluster-replicas 1(redis6.0.1)

redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6389 127.0.0.1:6390 127.0.0.1:6391 --cluster-replicas 1(redis6.0.1)

tmd。。。坑是真的多,总结一下

第一个坑:redis4.0.9版本太低了,而且ubnutu使用apt-get安装的话,受源的影响,只能安装redis4.0.9的。redis4.0.9我是死活找不到他的src目录,无奈去官网下了个redis6.0的

第二个坑:配置文件的每个字母都要打对。。。

第三个坑:开启redis实例,并没有找到生成的nodes-xxxx.conf。因为在配置文件中的cluster-config-file 这一段 我们没有设置他的生成路径,只填一个文件名称的话,他会自动生成在我们的系统根目录下。这个我也是一直排查了很久,最后才发现。我们的这个nodes-xxxx.conf应该放在我们的redisxxx.conf相同目录下。需注意

第四个坑:连接不上6380端口,因为我用的阿里云服务器,不仅系统里开启6380端口,而且安全组也要开放6380-6391端口

第五个坑:redis集群部署一直卡在Waiting for the cluster to join …。一开始我以为等他自己好,结果等了十几分钟。最后网上查阅才发现,不仅要开6379-6391这几个端口,还要开启16379-16391这几个端口。这叫集群总线端口。。。

第六个坑:[ERR] Node xxxxxxx:6380 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0. 这个错误比较显而易见,删除我们redis实例中的数据就好了。还有一点就是需要删除rdb,aof等文件。

13.4、通过redis-cli -c -p 6379 连接redis集群中的服务器

13.5、通过cluster nodes命令查看集群信息

13.6、redis cluster如何分配这六个节点

一个集群至少要有三个主节点

选项 --cluster -replicas 1表示我们希望为集群中的每个主节点创建一个从节点

分配原则尽量保证每个数据库运行在不同的ip地址,每个从库和主库不在一个ip地址上

13.7、什么是slots

一个redis集群中包含16384个插槽(hash slot),数据库的每个键都属于这16384个插槽中的一个

集群使用公式CRC16(key)%16384来计算键key属于哪个插槽,其中CRC16(key)语句用于计算键key的CRC16校验和

集群中的每个节点负责处理一部分插槽。

13.8、在集群中录入值

在redis-cli每次录入,查询键值,redis都会计算出该key应送往的插槽,如果不是该客户端对应的插槽,redis会报错,并告知应前往的redis实例和端口

redis-cli客户端提供了 -c参数实现自动重定向

不在一个slot下的键,是不能用mget,mset等多键操作。但是可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去

13.9、查询集群中的值

CLUSTER GETKEYSINSLOT 返回count个slot槽中的键

CLUSTER COUNTKEYSINSLOT 返回slot槽中键的个数

CLUSTER KEYSLOT CUST 返回键所在的插槽

13.10、故障恢复

如果主节点下线时间超过我们所设置的超时时间(我这里设置的是15秒),从节点就会自动升级成主节点。当主节点恢复后,主节点变成从节点。

如果某一段的插槽主从节点都宕掉,redis服务是否还能继续。

根据redis.conf中的cluster-require-full-coverage的值来判断,如果为yes,那么整个集群都会挂掉

如果为no,那么该插槽数据全都不能使用,也无法存储。

13.11、集群的Jedis开发

@Test
public void JedisDemo_01(){
    //创建对象
    HostAndPort hostAndPort = new HostAndPort("xxx.xx.xxx.xxx",6380);
    JedisCluster jedisCluster = new JedisCluster(hostAndPort);

    //进行操作
    jedisCluster.set("k2","v2");

    String value = jedisCluster.get("k2");
    System.out.println(value);
    jedisCluster.close();
}

踩了个坑:生成redis集群时,6379这个端口绑定了私网的IP地址,所以我们这里换成6380端口,反正他每个每个服务器都可以互相重定向

13.12、好处

实现扩容、分摊压力、无中心配置相对简单

13.13、不足

多键操作不被支持、多键的redis事务是不被支持的。lua脚本不被支持。

由于集群方案出现较晚,很多公司已经采用了其他集群方案,而代理或者客户端分片的想要迁移到redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

14、Redis6应用问题解决

14.1、缓存穿透

key对应的数据在数据源中并不存在,每次针对key的请求从缓存中获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。真有黑客咱先报警。

查询数据没有经过redis,直接去查询数据库,导致数据库访问量激增。

由以下原因导致:1、应用服务器压力变大。2、redis命中率变低。3、一直查询数据库。4、出现很多非正常的url访问

如何解决?

(1)对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍把这个空结果进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2)设置可访问的名单(白名单):使用bitmap类型定义一个可以访问的名单,名单id作为bigmap的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。有一个缺点,就是每次操作时都要访问一边bitmaps

(3)采用布隆过滤器:它实际是一个很长的二进制向量和一系列随机映射函数。将可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免对底层存储系统的查询压力。

(4)进行实时监控:当发现redis中的命中率开始急速降低,需要排查访问对象和访问数据,和运维人员配合,可以设置黑名单限制服务。

14.2、缓存击穿

数据库压力瞬间增大,redis里没有出现大量的key过期,redis正常运行

如何造成:1、redis中的某个key过期了,大量访问使用这个key,redis无法命中缓存,直接访问数据库,导致数据库压力增大。

解决方案:

(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据的key时长

(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长

(3)使用锁:就是在缓存失效的时候判断拿出来的值为空,不是立即去load db。先使用缓存工具的某些带成功操作返回值的操作去set一个mutext key。当操作返回成功时,再进行load db操作,并回设缓存,最后删除mutex key。当操作返回失败,证明有线程再load db,当前线程睡眠一段时间再重试,整个get缓存的方法。缺点就是效率很低,还是不用了。

(4)是否可以设置热点数据永不过期呢?

14.3、缓存雪崩

在极少的时间段内,大量的key集中过期

解决方案:(1)构建多级缓存架构:nginx缓存+redis缓存+其他缓存

(2)使用锁或队列:用加锁或者队列的方式保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,不适用高并发情况。

(3)设置过期标志更新缓存:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存

(4)将缓存失效时间分散开:比如我们可以在原有的失效时间基础上加上一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间重复率就会降低,就很难引发集体失效的事件。

14.4、分布式锁

1、随着业务的发展,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同服务器上,这使得原单机部署情况下的并发控制锁策略失效。这时候就需要一种跨jvm的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

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

(1)基于数据库实现分布式锁

(2)基于缓存(Redis等)

(3)基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点

(1)性能:redis最高

(2)可靠性:zookeeper最高

2、基于redis实现分布式锁

redis命令 setnx 命令在指定的 key 不存在时,为 key 设置指定的值。如果存在则不会覆盖

释放锁就是把key del掉

锁一直没有释放,设置key过期时间,自动释放

上锁后,还没来得及设置时间,服务器宕机。

set nx ex 这样既可以上锁又可以设置过期时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C4S5vsiu-1660038334058)(C:\Users\cyj\AppData\Roaming\Typora\typora-user-images\image-20220731155732505.png)]

15、Redis6新功能

16、八股文

1、redis事务是如何实现的

2、redis的内存淘汰机制

当内存满了的时候,Redis定义了几种策略来处理这些情况

(默认策略):对于写请求不再提供写服务,直接返回错误

allkeys-lru:从所有Key中使用lru算法进行淘汰

volatile-lru:从设置了过期时间的Key中使用LRU算法进行淘汰(LRU算法就是淘汰掉最近最少使用的key)

redis4.0中新增加一中淘汰策略,LFU算法,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。

LFU有两个策略

volatile-lfu:在设置了过期时间的key中使用LFU算法淘汰

allkeys-lfu:在所有Key中使用LFU算法淘汰数据

LRU和LFU的区别:我的理解是LRU是从没有时间段限制的去淘汰,而LFU是规定在一段时间内去进行淘汰。

allkeys-random:从所有key中随机淘汰数据

volatile-random:从设置了过期时间的key中随机淘汰

volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰

如何配置淘汰策略,在redis.conf中 加上这么一段 maxmemory-policy allkeys-lru

或者 在redis中输入这段命令 config set maxmemory-policy allkeys-lru

你可能感兴趣的:(redis,java)