redis学习笔记

Redis学习笔记

一、NoSQL数据库简介

1、解决问题

如何解决分布式下的session问题?

  1. 问题描述:假如有一个用户请求了某功能,Nginx第一次通过负载均衡,将这个请求分给了A服务器,此时存储用户信息的session位于A服务器中,而当用户再次请求时,Nginx将请求分给了B服务器,但此时B服务器没有session信息,如何解决这个问题?
  2. 解决方案:
    • 方案一:将用户信息存储到客户端cookie中,但是这种做法不安全
    • 方案二:session复制,将A服务器中的session复制给其他服务器,但是浪费时空间,并且数据冗余
    • 方案三:存储到数据库中,但是存在大量IO操作,影响性能
    • 方案四:存储到NoSQL数据库中,好处是不需要经过IO操作,完全可以存储在内存中,读写很快

2、NoSQL数据库

  1. 概念:Not Only SQL,泛指非关系型数据库,以key-value的形式存储。
  2. 不遵循SQL标准,不支持ACID,但是可以支持事务,查询效率高于SQL。
  3. 适用场景:
    • 对数据高并发的读写
    • 大量数据的读写
    • 对数据的高可扩展性
  4. 不适用场景:
    • 需要支持事务
    • 包含复杂关系的查询
  5. 常用的NoSQL数据库
    • Memcache:不支持持久化,支持的类型单一
    • Redis:可以持久化,几乎覆盖了Memcache的所有功能,并有自己独特的功能
    • MongoDB:文档型数据库,存储结构类似json,支持二进制数据和大型对象的存储

二、Redis6概述和安装

1、安装

  1. 进入Redis官网
  2. 点击即可下载

image-20210920103540432

  1. 安装c语言的环境
sudo apt install gcc
  1. 解压下载好的Redis压缩文件
tar -zxvf redis-x.x.x.tar.gz
  1. 进入redis目录,执行make命令
make
  1. 安装
make install
whereis
  1. 其他安装方式
sudo add-apt-repository ppa:redislabs/redis
sudo apt-get update
sudo apt-get install redis
ps -ef |grep redis
whereis redis

2、注意事项

​ 只支持Linux版本,因此不考虑在Windows下安装Redis。

3、Redis使用

  1. 前台启动
redis-server

image-20210920121334193

  1. 后台启动
  • 打开redis目录(我的是/etc/redis/),目录中有一个配置文件redis.conf
  • 将配置文件中的deamonize设置为yes
  • 命令启动并查看进程
redis-server /etc/redis/redis.conf
ps -ef | grep redis
  1. 用客户端访问并验证
redis-cli
ping
  1. redis关闭
  • redis-cli shutdown
  • 多实例关闭:redis-cli -p <端口号> shutdown

4、其他介绍

  1. 6379端口从何而来
    • Alessia Merz中Merz在九键键盘中的位置
  2. 数据库相关
    • 默认有0-15,共16个数据库
    • 初始默认使用0号库
    • 使用**select **来切换数据库
    • 密码统一管理,所有库密码相同
    • 使用dbsize查看当前数据库key的数量
    • 使用flushdb清空当前库
    • 使用flushall通杀全部库
  3. 单线程+多路IO复用
    • 与Memcache不同,Memcache使用了多线程+锁的机制,而Redis使用单线程+多路IO复用

三、常用五大数据类型

1、Redis的key操作

  1. **keys ***查看当前库中的所有key值
  2. **exists **查看当前库中是否含有这个key,返回结果为包含的记录数
  3. **type **查看该key的数据类型
  4. **del **删除某个key
  5. **expire
  6. **ttl **查看某个key距离过期剩余的时间,-1表示永不过期,-2表示已经过期

2、Redis字符串String

  1. String类型是二进制安全的,这就意味着可以用来存储图片,或者序列化对象
  2. String类型的value最大可以是512M
  3. **set **添加一条数据,如果重复设置相同的key,则会覆盖之前的key
  4. **append **在key的value值后面追加数据,返回值为新数据的长度
  5. **strlen **获取key对应的value长度
  6. **setnx **添加一条数据,只有在key不存在的时候,才能设置成功
  7. **incr **将key对应的value中的数字值加1,要求value为字符串格式的纯数字
  8. **decr **将key对应的value中的数字值减1,要求value为字符串格式的纯数字
  9. **incrby **将key对应的value增加increment
  10. **decrby **将key对应的value减少decrement
  11. 注意:redis操作是原子性的,不会被其他进程或线程打断
  12. 对于java而言,如果有两个线程分别对0进行++100次,最后结果的取值范围是多少?答:2-200
  13. **mset **批量添加数据
  14. **mget **批量获取数据
  15. **msetnx **批量添加数据,当且仅当所有给定的key都不存在
  16. **getrange **获取key对应value中start到end之间的字符串,类似于Java中的subString
  17. **setrange **覆盖写offset之后的数据,value有多长,就覆盖多少位
  18. **setex **设置键值对的同时,设置过期时间
  19. **getset **设置新值的同时获取旧值
  20. 底层数据结构是简单动态字符串(Simple Dynamic String,缩写SDS),底层类似Java的ArrayList

3、Redis列表List

  1. 底层是双链表(实际上是快速链表),当元素比较少的时候,会使用连续的区域存储,称为压缩链表,当数据很多时,会将多个压缩链表链接起来,叫快速链表

  2. 一个字符串列表,单键多值,可以添加元素到头部或尾部

  3. **lpush/rpush **向列表的左端/右端添加一个或多个值

  4. **lpop/rpop **在某个键的左端/右端弹出一个值,会使该值消失,当值都不存在的时候,键也就不存在了

    image-20210922090823439

  5. **lrange **查看某个键对应的值,按照索引下标查看该值得某些元素,当start=0,end=-1时,表示查看全部元素

    image-20210922090451626

  6. **rpoplpush **从key1右边弹出一个值,压到key2的左边

  7. **lindex **按照index下标获取元素,列表的下标从0开始

  8. **llen **获取某个键对应的值的长度

  9. **linsert before **在value前面插入newvalue

  10. **lrem **从左到右删除n个value

  11. **lset **将key下标为index的值替换成value

4、Redis集合Set

  1. 与list相似,可以去重,但是无序,底层为value=null的哈希表,添加、删除、查找的时间复杂度为O(1)
  2. **sadd **添加一个或多个值,如果已经存在key,则会被自动忽略掉
  3. **smembers **取出key对应的value中的所有值
  4. **sismember **判断key对应的集合中是否包含value,如果存在返回1,否则返回0
  5. **scard **返回该集合的元素个数
  6. **srem **删除key对应的集合中的某些值
  7. **spop **随机从key对应的集合中弹出一个值,这个值会从集合中删除
  8. **srandmember **随机从该集合中取出n个值,这些值不会从集合中删除
  9. **smove **把src集合中的value值移动到dst集合中
  10. **sinter **返回两个集合的交集元素
  11. **sunion **返回两个集合的并集元素
  12. **sdiff **返回两个集合的差集元素(属于key1,不属于key2的)

5、Redis哈希Hash

  1. Redis Hash是一个键值对集合,value是一个string类型的field和value的映射表,很适合存储对象
  2. **hset **给key集合中的field键赋值value
  3. **hget **取出key集合中field键对应的值
  4. **hmset **批量设置hash的值
  5. **hexists **判断给定的key集合中是否存在field键
  6. **hkeys **列出该hash集合中所有field
  7. **hvals **列出该hash集合中所有value
  8. **hincrby **为哈希表key中的域field的值加上增量increment
  9. **hsetnx **将哈希表key中的域field的值设置为value,当且仅当域field不存在

6、Redis有序集合Zset

  1. 与Set不同的是,Zset中的元素是有序的,Zset中的每个成员关联了一个评分(score),评分是排序标准,集合的成员是唯一的,但是评分是可以重复的
  2. **zadd **将一个或多个member元素及其score值加入到有序集key中
  3. **zrange [withscores]**返回有序集key中下标在start和end之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。
  4. **zrangebyscore [withscores] [limit offset count]**返回有序集key中,所有score值介于min和max之间([min, max])的成员,顺序为从小到大
  5. **zrevrangebyscore [withscores] [limit offset count]**同上,顺序为从大到小
  6. **zincrby **为元素的score加上增量
  7. **zrem **删除key集合下指定的元素
  8. **zcount **统计该集合中,分数区间内的元素个数
  9. **zrank **返回该value在集合中的排名,排名从0开始
  10. Zset底层使用了哈希表和跳跃表,其中哈希表的键存放value,值存放score,跳跃表用来排序,根据score对列表进行排序

四、Redis6配置文件详解

1、bind

bind 127.0.0.1表示,只支持linux本地连接redis,注释掉可以远程连接redis。

2、protected-mode

表示开启保护模式,yes表示只支持本机访问,no表示可以远程访问。

3、tcp-backlog

image-20210922145714458

4、timeout

在timeout秒之内没有对redis操作的时候,redis连接会关闭,默认为0,表示永不超时。

5、tcp-keepalive

redis是通过tcp连接的,tcp会每tcp-keepalive秒进行一次检测,检测有没有对redis进行操作,如果没有则会关闭连接。

6、daemonize

是否允许redis在后台运行,yes表示允许。

7、pidfile

redis在运行时,会把端口号设置到一个文件中,这个属性表示该文件的存放路径。

8、loglevel

日志级别。

9、databases

redis数据库的个数。

五、Redis6的发布和订阅

1、概念

​ Redis的发布和订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者接受(sub)消息。客户端可以订阅任意多的频道。

2、订阅

image-20210922152115553

3、发布

六、Redis6新数据类型

1、Bitmaps

  1. 不是一个数据类型,实际上就是一个字符串
  2. value是一个可以进行位操作的字符串,可以把Bitmaps想象成一个数组,数组中只能存储0和1,数组的下标在Bitmaps中叫偏移量
  3. **setbit **设置key中offset偏移量的值为value
  4. **getbit **获取key中offset对应的值
  5. **bitcount **统计key对应的value中1的个数,end=-1表示到最后一个,end=-2,表示到倒数第二个
  6. bitop对两个Bitmaps进行位操作

2、HyperLogLog

  1. 用于解决一些基数问题
  2. **pfadd **添加一个或多个值,可以去重
  3. **pfcount **统计key集合中元素的个数
  4. **pfmerge **将key1和key2的集合进行合并,合并结果放在key里面

3、Geospatial

  1. 元素到二维坐标,在地图上是经纬度
  2. Redis基于该类型提供了经纬度设置、查询、范围查询、距离查询、经纬度Hash等操作
  3. **geoadd **添加一个或多个地理位置
  4. geopos 取出key集合中某个元素到经纬度
  5. **geodist [m/km/ft/mi]**获取member1和member2之间的直线距离

七、Jedis操作Redis6

1、Jedis依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

2、连通性测试

private static final Jedis JEDIS = new Jedis("192.168.83.75", 6379);

public static void main(String[] args) {
    String value = JEDIS.ping();
    System.out.println(value);
}

3、常用操作

  1. key操作
@Test
public void test01() {
    // 删除一个键值对
    JEDIS.del("k1");
    // 添加一个键值对
    JEDIS.set("k1", "523");// 查询所有key
    Set<String> keys = JEDIS.keys("*");
    for (String key : keys) {
        System.out.println(key);
    }
    // 查询key的个数
    System.out.println(keys.size());
    // 判断是否存在某个key
    System.out.println(JEDIS.exists("k1"));
    // 查看key的存活时间
    System.out.println(JEDIS.ttl("k1"));
    // 查看key的value
    System.out.println(JEDIS.get("k1"));
}
  1. List操作
@Test
public void test02() {
    JEDIS.del("list1");
    // 添加一个列表
    JEDIS.lpush("list1", "Lucy", "Mary", "Jack");
    // 打印列表
    List<String> list1 = JEDIS.lrange("list1", 0, -1);
    for (String s : list1) {
        System.out.println(s);
    }
    // 弹出一个值
    String name = JEDIS.lpop("list1");
    System.out.println(name);
}
  1. Set操作
@Test
public void test03() {
    JEDIS.del("set1");
    // 添加一个set集合
    JEDIS.sadd("set1", "Lucy", "Mary", "Jack");
    // 打印一个Set集合
    Set<String> set1 = JEDIS.smembers("set1");
    for (String s : set1) {
        System.out.println(s);
    }
    // 删除Set集合中的某个元素
    JEDIS.srem("set1", "Lucy");
    // 打印删除后的Set集合
    Set<String> set2 = JEDIS.smembers("set1");
    for (String s : set2) {
        System.out.println(s);
    }
}
  1. Hash操作
@Test
public void test04() {
    JEDIS.del("user");
    // 添加记录
    Map<String, String> map = new HashMap<>(16);
    map.put("name", "Lucy");
    map.put("age", "20");
    map.put("gender", "female");
    JEDIS.hset("user", map);
    // 查询记录
    String name = JEDIS.hget("user", "name");
    System.out.println(name);
}
  1. Zset操作
@Test
public void test05() {
    JEDIS.del("zset1");
    // 添加记录
    Map<String, Double> map = new HashMap<>(16);
    map.put("Java", 500.0);
    map.put("C++", 400.0);
    map.put("Python", 300.0);
    JEDIS.zadd("zset1", map);
    // 查询记录
    Set<String> zset1 = JEDIS.zrange("zset1", 0, -1);
    for (String s : zset1) {
        System.out.println(s);
    }
}

4、验证码功能

要求:

  1. 输入手机号,点击发送后随机生成6位验证码,2分钟有效
  2. 输入验证码,点击验证,返回成功或失败
  3. 每个手机号每天只能输入3次

八、Redis6整合SpringBoot

1、添加依赖

<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>

2、配置项目配置文件

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

3、添加Redis配置类

/**
 * @author StarKing
 */
@EnableCaching
@Configuration
public class RedisConfig {
    /**
     * 配置redisTemplate
     * 默认情况下的模板只能支持 RedisTemplate<String,String>,
     * 只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> 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);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

4、测试

/**
 * @author StarKing
 */
@CrossOrigin
@RestController
@RequestMapping("/assemble")
public class AssembleController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/test")
    public String test() {
        // 设置值到Redis
        redisTemplate.opsForValue().set("assemble", "true");
        Object assemble = redisTemplate.opsForValue().get("assemble");
        return (String) assemble;
    }
}

九、Redis6事务操作

1、概念

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

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

2、Multi、Exec、Discard命令

​ 从输入Milti命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队过程中,可以通过Discard命令来放弃组队。

3、命令使用

image-20210923085935154

image-20210923085952522

4、错误处理

  1. 当组队阶段出现错误时,整个队列都会被取消,所有命令都不会被执行。

image-20210923090256425

  1. 当执行阶段出现错误时,只有出错的命令不会被执行,其他的会被执行

    image-20210923090347148

5、事务冲突

  1. 解决方案

image-20210923093735016

  1. 乐观锁命令

    • **watch **用来监视一个或多个key
    • 应当在开启事务(执行multi)之前执行该命令
    • 如果事务在执行(exec)之前,这个key被其他命令所改动,那么事务将会被打断
    • **unwatch **取消对一个或多个key的监视
  2. 演示

    • 终端1:

      image-20210923094431660

    • 终端2:

      image-20210923094448531

6、Redis事务三大特性

  1. 单独的隔离操作

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

  2. 没有隔离级别的概念

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

  3. 不保证原子性

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

7、秒杀案例

  1. 单线程下的代码
private boolean doKill(String userID, String prodID) {
    userID = "user_" + userID;
    // 商品数量
    String prodCount = "prod_" + prodID + "_count";
    // 商品清单
    String boughtList = "prod_" + prodID + "_bought";
    Object o = redisTemplate.opsForValue().get(prodCount);
    if (o == null) {
        // 秒杀还未开始
        System.out.println("秒杀未开始!");
        return false;
    }
    // 秒杀已经开始
    // 判断用户有没有重复秒杀
    Set<Object> members = redisTemplate.opsForSet().members(boughtList);
    if (members != null) {
        // 购物清单存在
        Boolean isMember = redisTemplate.opsForSet().isMember(boughtList, userID);
        if (isMember == null || isMember) {
            // 如果用户已经买过,返回false
            System.out.println("用户" + userID + "已经完成过秒杀!");
            return false;
        }
    }
    int curCount = (int) o;
    // 如果已经售空,则返回false
    if (curCount <= 0) {
        System.out.println("商品已被秒杀完!");
        return false;
    }
    // 开启事务,开始秒杀
    // 商品数减1
    redisTemplate.opsForValue().decrement(prodCount);
    // 添加商品到用户购物车
    redisTemplate.opsForSet().add(boughtList, userID);
    // 提交事务
    System.out.println("用户" + userID + "秒杀成功!");
    return true;
}
  1. 存在的问题

    在并发的情况下,会出现超卖问题。同时Redis有可能无法处理多个连接请求,因此会出现连接超时问题。

  2. 超卖和超时问题的解决

  1. 超卖问题可以采用同步机制解决

  2. 超时问题可以用Redis连接池来解决

十、Redis6持久化

1、两种方式

  1. RDB(Redis DataBase)
  2. AOF(Append Of File)

2、RDB

  1. RDB是什么?

    ​ 在指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot快照文件),恢复的时候,将快照文件读入到内存。

  2. 如何执行?

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

  3. RDB相关配置

    • RDB相关配置在redis.conf的SNAPSHOTTING块中
    • dbfilename:持久化文件的文件名
    • dir:持久化文件的路径
    • stop-writes-on-bgsave-error:当Redis无法写入磁盘时,直接关闭写操作
    • rdbcompression:持久化文件是否进行压缩存储
    • rdbchecksum:检查数据完整性
    • save:save秒钟写操作次数如果超过指定的次数,则进行一次持久化操作,手动保存
  4. RDB的优点

    • 适合大规模的数据恢复
    • 对数据的完整性和一致性要求不高时,比较适用
    • 节省磁盘空间
    • 恢复速度快
  5. RDB的缺点

    • Fork的时候会被克隆一份相同的数据,大致2倍的膨胀性需要考虑
    • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
    • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
  6. 如何停止持久化?

    动态停止RDB:redis-cli config set save “”,save后给空值,表示禁用保存策略

  7. RDB的备份

    拷贝rdb文件。

3、AOF

  1. AOF是什么?

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

  2. AOF持久化流程

    • 客户端的请求写命令会被append追加到AOF缓冲区;
    • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
    • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
    • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的。
  3. AOF默认不开启

    • 修改redis.conf文件中的appendonly为yes,开启AOF;

    • 可以在redis.conf中配置文件名称,默认为appendonly.aof;

    • AOF的保存路径与RDB的路径一致;

    • 重启后配置生效。

  4. AOF和RDB同时开启,Redis采用什么策略?

    系统默认取AOF的数据,因为数据不会存在丢失。

  5. 异常恢复

    • 修改默认的appendonly no 为yes
    • 如遇到AOF文件损坏,通过redis-check-aof --fix appendonly.aof进行恢复
    • 恢复:重启redis,然后重新加载
  6. AOF同步频率设置

    • appendfsync always,始终同步,每次写入操作都会记入日志,性能较差但完整性较好
    • appendfsync everysec,每秒记入一次,如果宕机可能丢失最后一秒的数据
    • appendfsync no,同步时机交给操作系统
  7. rewrite重写压缩

    • 只记录最后的结果值操作,将多条命令简化。

      image-20210930140008989

    • 重写条件:文件大小及基准值

image-20210930140213041

image-20210930140258201

  1. 优点
    • 备份机制更稳健,丢失数据概率更低
    • 可读的日志文件
  2. 缺点
    • 比RDB占用空间多
    • 恢复速度慢
    • 同步时存在性能压力

十一、Redis6的主从复制

1、简介

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

image-20210930141148810

好处:

读写分离:主服务器只进行写操作,从服务器只进行读操作。

容灾的快速恢复:当某一台从服务器挂掉的时候,可以从其他的从服务器读取数据。

2、配置一主多从

  1. 创建一个目录myredis存放各个Redis配置文件
  2. 复制一份配置文件到该目录下
  3. 创建新配置文件redis6379.conf
  4. 编辑redis6379.conf,内容如下:

image-20210930143322512

  1. 同样创建redis6380.conf、redis6381.conf
  2. 分别启动三个配置文件
  3. 启动结果如下:

image-20210930144237915

  1. 使用info replication打印主从复制的相关信息

image-20210930145837605

  1. 在从机上使用命令**slaveof **命令将该机设置为端口号为port的主机的从机

3、三大问题

  1. 一主两从

    • 从服务器挂掉时:过了某段时间后,该机又重新启动,此时该机会成为主服务器,需要重新设置该机为从服务器,但是数据不会有不同步、不一致的问题。
    • 主服务器挂掉时:从服务器可以知道主服务器挂掉了,自己还是从服务器。主服务器重启后,不需要从服务器做任何事情。
  2. 薪火相传

    • 从服务器也可以有从服务器,但是从服务器的从服务器不属于主服务器的从服务器。
    • 优点:有效减轻了master主机的写压力,降低中心化的风险
    • 缺点:一旦中间某个主机宕机,后续从机都无法得到备份
  3. 反客为主

    某个主服务器宕机时,可以在从机上通过slaveof no one命令将该从机设置为主机。

4、复制原理

  1. 从服务器连接上主服务器之后,从服务器会向主服务器发送请求数据同步的消息;
  2. 主服务器收到消息后,主服务器会将数据先进行持久化,然后将rdb文件发送给从服务器,从服务器会读取该rdb文件;
  3. 每次主服务器进行写操作之后,会和从服务器进行同步。

5、哨兵模式

  1. 概念:反客为主自动版,自动监控主机是否故障,如果故障则根据投票数自动将从服务器转为主服务器。
  2. 使用步骤:
    • 在/myredis下新建sentinel.conf文件
    • 在该文件中写一句sentinel monitor ,count为哨兵数量
    • 启动哨兵:redis-sentinel sentinel.conf

image-20210930160505124

  1. 主服务器恢复后,只能作从服务器。

十二、Redis6的集群

1、概念

  • 存在的问题:
    1. 容量不够时,Redis如何扩容?
    2. 并发写操作很多时,Redis如何分摊?
    3. 主从复制、薪火相传、主机宕机导致的IP地址发生变化时,程序中配置需要对应修改,如何解决?
  • 解决方案:由主机代理方式转为无中心化集群配置。
  • 主机代理:客户端访问代理服务器,代理服务器寻找对应的Redis服务,进行写操作。
  • 无中性化集群:各个服务器都可以作为访问入口,各个服务器可以相互访问到。

2、集群搭建

搭建一个三主三从的集群,主机端口号为6379、6380、6381,从机端口号为6389、6390、6391。

当然在工作中,每个Redis服务都会各自占用一台服务器。

  1. 编写redis6379.conf

    image-20211006111209395

  2. 复制其他配置文件

    image-20211006111402419

  3. 配置每个文件中的端口号等属性

  4. 启动六个Redis服务

    image-20211006111955255

  5. 将六个节点合成集群

    redis-cli --cluster create --cluster-replicas 1 172.25.191.198:6379 172.25.191.198:6380 172.25.191.198:6381 172.25.191.198:6389 172.25.191.198:6390 172.25.191.198:6391
    

    其中,–cluster-replicas表示选择搭建方式,1表示以最简单的方式搭建集群。

    image-20211006113940471

    image-20211006114001494

    如果有以上的效果,表示已经完成了三主三从的集群搭建。

  6. 连接集群,进行测试

    使用命令**redis-cli -c -p **连接集群。

    使用cluster nodes查看集群信息。

    image-20211006114445719

3、集群如何分配节点?

  1. 一个集群中至少要有三个主节点。
  2. –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
  3. 分配原则:尽量保证每个主数据库运行在不同的IP地址,每个从数据库和主库不在一个IP地址

4、什么是slots?

​ 搭建集群时,成功后会显示一句**[OK] All 16384 slots covered**,这里的16384就是slot的个数,数据库中的每一个键都属于这16384个插槽的其中一个。

​ 集群使用公式**CRC16(key) %**来计算键key属于哪个槽。

​ 集群中每个节点负责处理一部分插槽,例如A节点负责0-5460,B节点负责5461-10922,C节点负责10923-16383。当使用set命令向数据库中插入值的时候,会用这个set的key先进行计算,然后根据计算结果,确定在哪个服务器中加入数据。

image-20211006120444987

5、常用命令

  1. **cluster keyslot **查询某个key的slot值
  2. **cluster countkeysinslot **查询slot对应位置中有多少个值,注意查询前需要转到对应的服务器

6、故障恢复

​ 主机挂掉之后,从机会上位。主机重启后,会变为从机。

​ 如果主从机都挂掉了,集群能否正常工作取决于cluster-require-full-coverage,如果该值为yes,表示某个节点挂掉之后,整个集群会瘫痪;如果该值为no,表示只有该节点不能提供服务。

7、Jedis的集群开发

/**
 * @author StarKing
 */
public class ColonyTest {
    public static void main(String[] args) {
        // 端口号可以是任意一个,因为是无中心化的
        HostAndPort hostAndPort = new HostAndPort("172.25.191.198", 6379);
        JedisCluster jedisCluster = new JedisCluster(hostAndPort);
        // jedisCluster包含了所有的操作
        jedisCluster.set("k1", "v1");
    }
}

8、好处和不足

  1. 好处
    • 实现了Redis扩容
    • 分摊了并发压力
    • 无中心化配置,相对简单
  2. 不足
    • 不支持多键操作,如多键添加等
    • 多键的Redis事务不被支持

十三、Redis6的应用问题解决

1、缓存穿透

  1. 问题描述
    • 当应用服务器压力突然变大时,Redis缓存命中率降低,导致服务器需要不断查询数据库,数据库压力增大。此时缓存内部依旧平稳运行,但是查询的数据都是在数据库中。
    • Redis查询不到数据,或者出现很多非正常URL访问时,会有缓存穿透的现象

image-20211007112317539

  1. 解决方案
    • 对空值缓存:如果一个查询返回的数据为空,那么依然将这个空结果缓存,但设置空结果的过期时间很短,最长不超过五分钟。
    • 设置可访问名单:使用bitmaps定义一个可访问名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果不在bitmaps里面,则进行拦截。
    • 采用布隆过滤器
    • 实时监控Redis:监控Redis的命中率,需要排查访问的对象和访问的数据,从而设置黑名单限制服务。

2、缓存击穿

  1. 问题描述
    • 数据库访问压力瞬时增加,出现崩溃情况,但Redis里面没有出现大量key过期,Redis仍然正常运行。
    • Redis某个热门key过期,大量访问使用这个key

image-20211007122236231

  1. 解决方案
    • 预先设置热门数据:在Redis访问高峰之前,把一些热门数据提前存入到Redis里面,加大这些热门数据的存活时间。
    • 实时调整:现场监控哪些数据热门,实时调整这些数据的存活时间。
    • 使用锁:如果查询缓存,得到结果为空,则设置锁,让其不断查询,知道查询到结果为止。

3、缓存雪崩

  1. 问题描述
    • 服务器压力变大,服务器崩溃
    • 在极少的时间段内,查询大量key的集中过期情况,Redis命中率降低,则访问数据库压力增大。

image-20211007123347850

  1. 解决方案
    • 构造多级缓存
    • 使用锁或队列:保证不会有大量的线程对数据库进行一次性的读写,不适用高并发场景。
    • 设置过期标志更新缓存:记录过期时间,并倒计时,当这个过期时间结束后,通知后台线程更新缓存内容。
    • 将缓存失效时间分散开:比如将一个键的失效时间设置为5分01秒,另外一个设置为5分02秒,以此类推,这样就不会出现同时失效的情况。

4、分布式锁

  1. 问题描述

    ​ 单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下单并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题,就需要一种跨JVM的互斥机制来控制共享资源的访问。

  2. 解决方案:

    1. 基于数据库实现分布式锁
    2. 基于缓存(Redis等)实现——性能最高
    3. 基于Zookeeper——可靠性最高
  3. 使用Redis实现分布式锁

    1. 方式一:通过setnx 设置锁,只有当释放(del )锁后,其他操作者才可以进行操作。但是这种方法存在问题,如果某个操作者一直占用锁,那么将会陷入无期限阻塞
    2. 方式二:在方案一点基础上,设置锁的过期时间(expire ),可以解决无期限阻塞问题。此时,无法保证原子性,即setnx和expire命令不一定一起执行,可能会被打断。
    3. 方式三:使用命令set nx ex ,在设置键值的同时上锁,并且设置了存活时间。
  4. 代码实现

@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/request")
    public String request() throws InterruptedException {
        // setnx获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "10"3, TimeUnit.SECONDS);
        if (lock != null && lock) {
            // 如果获取到锁
            // 执行操作
            System.out.println("执行了相关操作!");
            // 释放锁
            redisTemplate.delete("lock");
        } else {
            // 如果没有获取到锁,即锁被其他线程占用
            TimeUnit.SECONDS.sleep(1);
            return request();
        }
        return "";
    }
}

​ 上面的代码存在锁误删问题:假如有两台服务器,第一台获取到锁之后,进行了一段时间的卡顿,假如卡顿了4秒,而锁的存活时间为3秒,因此在第3秒的时候,锁被释放,此时B服务器获取到了该锁,需要后续进行2秒的操作,但是操作到第1秒的时候,A服务器将锁手动释放掉,也就是说此时B服务器的锁就会被释放。

锁误删问题解决方案:使用UUID解决,保证每个服务器释放的都是自己的锁,即释放锁的时候进行判断,判断当前的UUID和要释放掉UUID是否一致,如果一致才释放,需要注意的是,必须保证这个判断是一个原子操作

​ 那么如何实现该判断是原子操作?我们引入LUA脚本,LUA脚本是一款嵌入式语言,它的操作可以保证原子性。由此我们得到最终版本:

@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/request")
    public String request() throws InterruptedException {
        // setnx获取锁
        String uuid = UUID.randomUUID().toString();
        // 商品ID
        int prodId = 10;
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock" + prodId, uuid, 3, TimeUnit.SECONDS);
        if (lock != null && lock) {
            // 如果获取到锁
            // 执行操作
            System.out.println("执行了相关操作!");
            // 使用LUA脚本释放锁
            String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 使用Redis执行LUA脚本
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(lua);
            redisScript.setResultType(Long.class);
            redisTemplate.execute(redisScript, Collections.singletonList("lock" + prodId), uuid);
        } else {
            // 如果没有获取到锁,即锁被其他线程占用
            TimeUnit.SECONDS.sleep(1);
            return request();
        }
        return "true";
    }
}
尚硅谷是一个教育机构,他们提供了一份关于Redis学习笔记。根据提供的引用内容,我们可以了解到他们提到了一些关于Redis配置和使用的内容。 首先,在引用中提到了通过执行命令"vi /redis-6.2.6/redis.conf"来编辑Redis配置文件。这个命令可以让你进入只读模式来查询"daemonize"配置项的位置。 在引用中提到了Redis会根据键值计算出应该送往的插槽,并且如果不是该客户端对应服务器的插槽,Redis会报错并告知应该前往的Redis实例的地址和端口。 在引用中提到了通过修改Redis的配置文件来指定Redis的日志文件位置。可以使用命令"sudo vim /etc/redis.conf"来编辑Redis的配置文件,并且在文件中指定日志文件的位置。 通过这些引用内容,我们可以得出结论,尚硅谷的Redis学习笔记涵盖了关于Redis的配置和使用的内容,并提供了一些相关的命令和操作示例。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis学习笔记--尚硅谷](https://blog.csdn.net/HHCS231/article/details/123637379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis学习笔记——尚硅谷](https://blog.csdn.net/qq_48092631/article/details/129662119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值