Redis

Redis

一、Nosql

  1. 为这么要使用Nosql:
    • 因为当下数据量和访问量很大,一个机器放不下,内存不够
    • 使用缓存,即可将一些数据放入缓存中,用户查询时就可以在缓存中找到数据,而不用在取数据库中查询数据
  2. Nosql特点:
    • Redis是单线程+io多路复用,所以具有原子性(不同的操作不会被打断)
    • (解耦)方便扩展
    • 大数据量高性能
    • 数据类型多样性,不需要设计数据库
    • 传统RDBMS和Nosql
    • 不支持ACID
  3. Nosql的四大分类:
    • KV键值对:Redis
    • 文档数据类型:MongoDB
    • 列存储数据库
    • 图关系数据库

二、Redis入门

1.安装

官网:https://redis.io/download

  • 将下载文件拖入到linux中

  • 解压文件:

    tar -zxvf  压缩包名称
    
  • 进入解压后的文件

    cd  解压后的文件名称
    
  • 使用命令编译(必须要有gcc 使用:gcc --version 查看)

    make
    
  • 安装

    make install
    
  • 完成,跳转到 /usr/local/bin/ 目录下即可看到redis相关文件

  • 修改redis.conf配置文件

    daemonize yes
    
  • 后台启动

    cd /usr/local/bin
    redis-server ~/redis/redis6.2.3/redis.conf
    

2.五大基本数据类型

0.key 操作

  • 查看所有key

    keys *
    
  • 设置key - value

    set key1 tom
    set key2 jerry
    set ket3 jack
    
  • 查看是否存在key

    exists key1
    
  • 查看key的类型

    type key1
    
  • 删除key(直接删除)

    del key3
    
  • 删除key(异步删除:通知用户已经删除,实际没有,后续慢慢删除)

    unlink key3
    
  • 设置key的有效期(单位:s)

    expire key1 10
    
  • 查看key的有效期(返回剩余时间;过期时返回-2;永不过期:-1)

    ttl key1
    
  • 切换库(默认0)

    select 1
    
  • 查看库中的key的数量

    dbsize
    
  • 清空当前库

    flushdb
    

1.字符串 String

String类型可以存放任何数据,最多存放512M

  • 设置 key - value

    # 1.直接添加,如果不存在key则添加key,如果已存在key则替换value
    set key value
    # 如:
    set k1 v1000
    
    # 2.当key不存在时才能添加
    setnx key value
    setnx k1 v2000   # 失败
    setnx k2 v2000   # 成功
    
  • 获取值

    get key
    # 如:
    get k1
    
  • 字符串追加

    append key 	value
    # 如:
    append k1 111
    
  • 获取长度

    strlen key
    # 如:
    strlen k1
    
  • 对数字值的 +1 或 -1

    # +1
    incr key
    # -1
    decr key
    
  • 对数字值的 +任意值 或 -任意值

    # 加
    incrby key number
    # 如:+10
    incrby k3 10
    
    # 减
    decrby key number
    # -50
    decrby k3 50
    
  • 同时设置多个 key - value

    mset key value key value ...
    msetnx key value key value  # 只能当key不存在时才能设置
    
  • 同时得到多个value

    mget key key key
    
  • 根据范围设置或取值

    set name jerry
    getrange name 0 3
    setrange name 1 ---  # 得到j---y,依次向后覆盖
    
  • 在设置值的时候添加过期使劲按

    setex key second value
    # 如下:
    setex gender 20 man
    
  • 取旧值,设新值

    getset key value
    # 如:
    getset name tom #返回的是原来的值,但是该key的值已经变为tom
    

2.列表 List

list是一个双向链表(有序)

在这里插入图片描述

  • lpush:从左边加入

    lpush list1 v1 v2 v3 v4
    

    取出时, 显示顺序是:v4 v3 v2 v1;因为是从左边添加,添加时,所有的value会被向右移一个位置。

  • rpush:从右边加入

    rpush list2 v1 v2 v3 v4
    

    取出时, 显示顺序是:v1 v2 v3 v4;因为从右边添加,添加时,所有的value会被向左移一个位置。

  • lpop:从左边弹出一个值

    lpop list1
    
  • rpop:从右边弹出一个值

    rpop list1
    
  • rpoplpush:弹出一个key的右边的值加入到另一个key的左边

    rpoplpush key1 key2
    

    表示从key1的右边弹出一个值,加入到key2的左边

  • lrange:取值

    range key 0 -1 # 表示取全部的值
    
  • lindex:按下标取值

    lindex key 0 # 取下标为0的值
    
  • llen:列表长度

    llen key
    
  • linsert:在某一个值的前面或后面添加新值

    linsert key before/after value newvalue
    
  • lrem:删除前面的几个value(从左往右删除)

    lrem key 2 value
    # 如:
    lrem list1 2 v1  # 表示删除前面的量v1的值(不足则全部删除)
    
  • lset:替换

    lset key index value
    # 如:
    lset list1 1 v100 #表示将下标为1的值替换为v100
    

底层原理:

在这里插入图片描述

Redis底层使用QuickList,由ziplist组成。当只有少量数据的时候,会申请一段连续的内粗空间,为ziplist。随着数据量的增加,会将多个ziplist使用链表指针连接起来组成quicklist。因为双向链表的指针会占用大量的空间。使用quickList会节省大量的空间。

3.集合 Set

即无序、不重复。底层是一个value为null的hash表。

  • sadd:添加一个或多个值

    sadd key value value value ... # 重复的值会被自动排除
    
  • smembers:取出set中的全部值

    smembers key
    
  • sismember:判断set中是否有某个值,有返回1,没有返回0

    sismember key value
    
  • scard:返回集合的元素个数

    scard key
    
  • srem:删除集合中的一个或几个元素

    srem key value value .... # 删除一个或几个值
    
  • spop:随机从集合中弹出一个值

    spop key count # 随机弹出count个值
    
  • srandmember:随机从集合中取个n值,不会删除

    srandmember key count # 表示从一个集合中随机取count个值
    
  • smove:把集合中的一个值移动到一个另一个集合中

    smove key1 key2 value   # 把key1中的value移动到key2中
    
  • sinter:返回两个集合的交集

    sinter key1 key2
    
  • sunion:返回两个集合的并集

    sunion key1 key2
    
  • sdiff:返回两个集合中的差集(key1 中有,key2中没有的值 )

    sdiff key1 key1  
    

底层结构:

与java中HashSet与HashMap的关系类似

4.哈希 Hash

hash中的values使用key-value的映射

在这里插入图片描述

  • hset:设置值

    hset key field value
    # 如:添加key为user field为id value为1
    hset user id 1
    
  • hget:取值

    hget key field
    # 如:获取id
    hget user id
    
  • hmset:批量设置值

    hmset key field value field value
    # 如:
    hmset user2 id 2 name lisi age 20
    
  • hexists:判断key中是否有field

    hexists key field
    # 如:判断user2中是否有name
    hexists user2 name
    
  • hkeys:列出该key中所有的filed

    hkeys key
    
  • hvals:列出该key中所有value

    hvals key
    
  • hincrby:为key中的指定filed +1

    hincrby key field increment
    # 如:年龄+5
    hincrby user2 age 5
    
  • hsetnx:为key中添加field-value(只有当该field不存在时)

    hsetnx key field value
    

5.有序集合Zset

Zset和set类似,都会自动排重;不同的是,Zset的value具有评分,可以根据评分来排序

  • zadd:添加

    zadd key score value score value ...
    # 如:
    zadd key1 8 z1 9 z2 10 z3
    
  • zrange:查询value

    zrange key start stop
    # 如:显示全部
    zrange key1 0 -1
    # 显示评分
    zrange key1 0 -1 withscores
    # 根据评分的范围显示(5 - 10)
    zrangebyscore key1 5 10
    # 根据评分范围反向显示
    zrevrangebyscore key1 10 0
    
  • zincrby:更改评分

    zincrby key increment value
    
  • zrem:删除元素

    zrem key value
    
  • zcount:统计评分范围内的元素个数

    zcount key min max
    
  • zrank:查看元素在集合中的排名(从0开始)

    zrank key value
    

底层原理:

使用hash(方便存储score和value)+跳跃表(快速查找)实现

3.配置文件

4.发布订阅

即一种通信模式,消息的订阅者可以收到消息发布者发布的消息。

Redis可以订阅任意频道的消息。

实现:

客户端1:

# 订阅
subscribe channel1

客户端2:

# 发布
publish channel1 hello

当客户端2通过channel1发布消息后,客户端1会收到该消息。

5.Redis6中的新数据类型

1.Bitmaps

在这里插入图片描述

  • setbit:根据偏移量添加

    setbit key offset value
    
  • getbit:根据偏移量,获取该位置是否置为1

    getvit key offset
    
  • bitcount:统计置为1的位数个数

    bitcount key [start,end]
    
  • bitop:取并集(and)、交集(or)

2.HyperLogLog

用于统计不同元素的基数(占用空间小)

可以做到去重

  • pfadd:添加基数

    pfadd key value value value ...
    
  • pfcount:统计基数个数

    pfcount key
    
  • pfmerge:将两个HyperLogLog的value复制到一个HyperLogLog中

    # 将key1、key2中的value添加到key3中
    pfmerge key3 key1 key2
    

3.Geospatial

即地理坐标,用于存放二维坐标

  • geoadd:添加

    geoadd key longitude latitude member
    # 如:添加上海和重庆的坐标
    geoadd china:city 121 31 "shanghai" 106 29 chongqing
    
  • geopos:获取member的坐标

    geopos key member
    # 如:获取上海的坐标
    geopos china:city shanghai
    
  • geodist:获取d两地的直线距离

    geodist key member1 member2
    # 如:获取上海与重庆的直线距离
    geodist china:city shanghai chongqing
    
  • georadius:获取以某个点为中心,半径内的点

    georadius key longitude latitude radius m|km
    
    # 如:获取以 110 30 为中心,半径1000km的范围内的点
    georadius china:city 110 30 1000 km
    

6.Jedis操作

类似于JDBC,可以使用java连接redis

注意问题

  • redis.conf配置文件的设置

    1.注释如下配置,否则只能本地访问(linux内部)

    #bind 127.0.0.1 -::1
    

    2.修改模式 yes改为no

    protected-mode no
    

    3.远程连接无法访问,需要关闭linux防火墙

    systemctl stop firewalld
    

1.Redis连接测试

导入gav:

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

java代码:

public class demo01 {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("49.234.85.106",6379);
        String ping = jedis.ping();
        System.out.println(ping);
    }
}

// 输出:
// PONG

2.数据命令的简单操作

    @Test
    public void test1(){
        Jedis jedis = new Jedis("192.168.1.118",6379);
        /*==========String==============*/
        jedis.set("name", "zhangsan");
        String name = jedis.get("name");
        System.out.println(name);

        jedis.append("name",  "ni hao");
        String name1 = jedis.get("name");
        System.out.println(name1);

        Boolean exists = jedis.exists("name");
        System.out.println(exists);

        /*=============set==========*/
        jedis.sadd("s1", "v1","v2","v3");
        Set<String> s1 = jedis.smembers("s1");
        for (String s : s1){
            System.out.print(s + "   ");
        }
        System.out.println();


        /*=============list===========*/
        jedis.lpush("list1", "v1","v2","v3");
        for(int i=0; i<=jedis.llen("list1"); i++){
            System.out.print(jedis.lpop("list1") + "   ");
        }
        System.out.println();
    }

3.简单场景使用

场景:手机验证码

  • 手机验证码6位有效期为120s
  • 每个手机每天只能申请3次
  • 成功返回成功,失败返回失败

环境:springboot

业务类:

@Service
public class util {

    // 随机生成6为验证码
    public String getPhoneCode(){
        Random random = new Random();
        StringBuilder str = new StringBuilder();
        for(int i=0; i<6; i++){
            str.append(random.nextInt(10));
        }
        System.out.println(str.toString());
        return str.toString();
    }


    // 发送验证码
    public boolean connectRedis(String phone, String code){
        // 连接redis
        Jedis jedis = new Jedis("192.168.1.118",6379);

        // 约定每个手机号每天的发送次数的key
        String countKey = "countKey:"+phone;
        // 约定验证码的key
        String codeKey = "codeKey:"+phone;

        // 获取发送次数
        String s = jedis.get(countKey);

        if(s == null){  // 第一次发送
            jedis.setex(countKey, 60*60*24,"1");
            jedis.setex(codeKey,120,code);
        }else if(Integer.valueOf(s) < 3){   // 不超过三次
            jedis.incr(countKey);
            jedis.setex(codeKey,120,code);
        }else{   // 超过三次
            System.out.println("超过三次了");
            return false;
        }
        jedis.close();
        return true;
    }


    // 验证
    public boolean verification(String phone,String code){
        // 连接redis
        Jedis jedis = new Jedis("192.168.1.118",6379);

        // 约定验证码的key
        String codeKey = "codeKey:"+phone;

        // 获取验证码信息
        String s = jedis.get(codeKey);

        System.out.println(s);
        try{
            if(s == null){
                return false;
            }
            if(s.equals(code)){
                return true;
            }
            return false;
        }finally {
            jedis.close();
        }
    }
}

contoller:

@RestController
public class PhoneCodeController {

    @Autowired
    private util util;

    @GetMapping("/get/code/{phoneNumber}")
    public String getCode(@PathVariable(value = "phoneNumber")String phoneNumber){
        String code = util.getPhoneCode();
        boolean b = util.connectRedis(phoneNumber, code);
        if(b){
            return code;
        }else{
            return "尝试次数太多,明天再来吧~";
        }

    }

    @PostMapping("/post/code/{phoneNumber}/{code}")
    public String postCode(
            @PathVariable(value = "phoneNumber")String phoneNumber,
            @PathVariable(value = "code")String code){

        boolean verification = util.verification(phoneNumber, code);
        if(verification){
            return "验证成功!";
        }else{
            return "验证失败~";
        }
    }

}

获取验证码:

在这里插入图片描述

验证:

在这里插入图片描述

4.springboot整合Redis

在第三步中使用的是jedis直接连接,下面将使用springboot整合redis

1.依赖

        <!--redis相关-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.3.10.RELEASE</version>
        </dependency>
        <!--集成redis-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.2</version>
        </dependency>

2.yml配置文件

spring:
  redis:
    # redis 服务器地址
    host: 192.168.1.118
    # 服务器端口号
    port: 6379
    # 数据库索引(默认为0)
    database: 0
    # 连接超时时间
    timeout: 1800000

    password: 123456
    lettuce:
      pool:
        # 连接池最大连接数,负值表示没有限制
        max-active: 20
        # 最大空间连接
        max-idle: 5
        # 最小空闲连接
        min-idle: 0
        # 最大阻塞等待时间
        max-wait: -1

3.自定义RedisTemplate

如果使用默认的redisTemplate,则在添加key或value时不能被序列化,所以需要自定义

@Configuration
public class RedisConfig {
    
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 设置key的序列化方式:字符串的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        // 设置value的序列化方式:json
        Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        template.setValueSerializer(redisSerializer);
        template.setHashValueSerializer(redisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

4.测试

@RestController
public class RedisDemo1 {
    @Autowired
    private RedisTemplate redisTemplate;
    @GetMapping("/get/redis")
    public String getRedis(){
        redisTemplate.opsForValue().set("test", "测试");
        Object test = redisTemplate.opsForValue().get("test");
        return (String)test;
    }
}

此时,能够得到value且放入redis中后不会被转义

在使用时,可以使用util工具类

7.事务

1.事务概述

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。

过程示意图:

在这里插入图片描述

失败的两种情况:

一、组队时错误

在这里插入图片描述

在这里插入图片描述

当在组队阶段有错误时,全部命令都会失败

二、执行时错误

在这里插入图片描述

在这里插入图片描述

当在执行命令时出错,只会时该条命令失败,其他不受影响

2.事务冲突

在实际中,存在事务的冲突问题:

例如:一个账户,在三个设备”同时“转出金额,但是转出之和大于余额,就会导致事务冲突。

在redis中,使用锁的机制来解决这些问题。

1.悲观锁

在这里插入图片描述

2.乐观锁

在这里插入图片描述

使用wathc命令,实现乐观锁

客户端1:

127.0.0.1:6379> set name "zhangsan"
OK
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name "lisi"
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379> get name
"lisi"
127.0.0.1:6379> 

客户端2:

127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name "wangwu"
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get name
"lisi"
127.0.0.1:6379> 

3.事务使用案例

背景:秒杀系统,在高并发的情况下,会出先数据错误,如下:

业务逻辑:

某商品实行秒杀促销,库存有10个;数据库中有两个数据:proNum:user:proid表示库存,user:proid表示抢到的用户信息。

@Service
public class Redis02Service {
    @Resource(name = "redisTemplate1")
    private RedisTemplate redisTemplate;

    public boolean doSecKill(String userId, String proId){
        // 1.判断userId、ProId是否为空
        if(userId == null || proId == null){
            System.out.println("用户错误~");
            return false;
        }
        // 2.拼接key
        String proNumKey = "proNum:" + proId;
        String userKey = "user:" + proId;

        // 3.判断proId是否正确、查询库存
        Integer proNum = (Integer)redisTemplate.opsForValue().get(proNumKey);
        if (proNum == null){
            System.out.println("秒杀不存在~");
            return false;
        }
        if(Integer.valueOf(proNum) < 1 ){
            System.out.println("来晚了,结束了~");
            return false;
        }
        // 4.判断是否重复秒杀
        Boolean member = redisTemplate.opsForSet().isMember(userKey, userId);
        if (member){
            System.out.println("已近抢过了,下次再来~");
            return false;
        }
        // 5.秒杀
        // 库存-1
        redisTemplate.opsForValue().decrement(proNumKey);
        // 添加用户列表
        redisTemplate.opsForSet().add(userKey,userId);
        System.out.println("秒杀成功!");
        return true;
    }
}

controller:

为方便实现高并发,用户id采用随机方式。

@RestController
public class RedisController {
    @Autowired
    private Redis02Service redis02Service;

//    @GetMapping("/seckill/{userId}/{proId}")
    @GetMapping("/seckill")
    public String secKill(
//            @PathVariable(value = "userId")String userId,
//            @PathVariable(value = "proId")String proId
    ){
        int userId = new Random().nextInt(50000);
        boolean isSuccess = redis02Service.doSecKill(String.valueOf(userId), "1");

        if(isSuccess){
            return "秒杀成功!";
        }
        return "秒杀失败";
    }
}

高并发:使用JMETER实现每秒钟2000个请求。

结果:

在这里插入图片描述

秒杀过程出现错误:顺序完全出错。

在这里插入图片描述

库存为负数,超卖现象。

解决:使用事务。

这里使用的是jedis(RedisTemplate不会。。)

修改后的业务代码:

@Service
public class Redis02Service2 {
    public boolean doSecKill(String userId, String proId){
        // 1.判断userId、ProId是否为空
        if(userId == null || proId == null){
            System.out.println("用户错误~");
            return false;
        }
        // 2.拼接key
        String proNumKey = "proNum:" + proId;
        String userKey = "user:" + proId;

        // 连接redis
        Jedis jedis = new Jedis("192.168.1.119", 6379);

        // 3.判断proId是否正确、查询库存
        String proNum = jedis.get(proNumKey);
        if (proNum == null){
            System.out.println("秒杀不存在~");
            jedis.close();
            return false;
        }
        if(Integer.valueOf(proNum) <= 0 ){
            System.out.println("来晚了,结束了~");
            jedis.close();
            return false;
        }

        // 监视库存
        jedis.watch(proNumKey);

        // 4.判断是否重复秒杀
        Boolean sismember = jedis.sismember(userKey, userId);
        if (sismember){
            System.out.println("已近抢过了,下次再来~");
            jedis.close();
            return false;
        }

        // 开启事务
        Transaction multi = jedis.multi();
        // 命令入队
        multi.decr(proNumKey);
        multi.sadd(userKey,userId);
        // 事务提交
        List<Object> exec = multi.exec();

        // 如果返回的为空,或者操作过成功的命令为0,则表示做错失败
        if(exec != null && exec.size()>0){
            System.out.println("秒杀成功!");
            jedis.close();
            return true;
        }
        return false;
    }
}

4.库存遗留问题

由于乐观锁的机制,版本号的频繁改变,所以会导致控制台显示抢完时,实际数据库中还有剩余。

使用LUA脚本解决

8.持久化操作

持久化:即,将数据保存到磁盘。

1.RDB

机制:每隔一段时间,将数据集快照持久化到磁盘。

redis会创建一个子进程(fork)来进行持久化操作。持久化时,先将数据写入到临时文件,再将持久化好的临时文件替换上次持久化好的持久化文件。写时复制技术

适用于大规模数据恢复。

缺点:最后一次持久化的数据可能会丢失。占用两倍的空间所以适用于对数据恢复完整性要求不高的环境,否则应该使用AOF。原因如下:

在这里插入图片描述

配置文件解读:redis.conf

1.持久化规则:

  • 一小时内至少有一个key改变
  • 5分钟内至少有100个key改变
  • 1分钟内至少有10000个key改变

三者满足其一即持久化

    374 # Unless specified otherwise, by default Redis will save the DB:
    375 #   * After 3600 seconds (an hour) if at least 1 key changed
    376 #   * After 300 seconds (5 minutes) if at least 100 keys changed
    377 #   * After 60 seconds if at least 10000 keys changed
    378 #
    379 # You can set these explicitly by uncommenting the three following lines.
    380 #
    381 # save 3600 1
    382 # save 300 100
    383 # save 60 10000

2.持久化文件名

默认持久化名称为dump.rdb

    430 # The filename where to dump the DB
    431 dbfilename dump.rdb

3.持久化文件位置

即:保存在当前目录下(在哪个目录下启动redis,就在哪个目录下生成持久文件)

    451 # The Append Only File will also be created inside this directory.
    452 #
    453 # Note that you must specify a directory here, not a file name.
    454 dir ./

4.当磁盘已满时,关闭redis写操作

stop-writes-on-bgsave-error yes

5.检查完整性

rdbchecksum yes

RDB恢复操作:RDB恢复会根据持久化的文件进行自动恢复,但可能会丢失最后一次的持久化数据(如上所述)

2.AOF

AOF以日志的形式来记录每个写操作(增量保存),记录所有写操作(增、删、改),只追加文件,不修改文件。Redis启动时就会读取该文件以恢复数据。(会记录命令)

AOF默认不开启。

开启:

appendonly yes   # 开启AOF

# The name of the append only file (default: "appendonly.aof")
# 持久化文件名称
appendfilename "appendonly.aof"

RDB生成的文件在哪,AOF生成的文件也在哪

AOD/RDB同时开启,默认使用aof的数据

数据恢复:与RDB一样,AOF启动数据库时,也对读取响应的文件以恢复数据。

异常修复:当appendonly.aof文件损坏(异常)时,可以对其进行修复。

如下:

1.在文件添加错误信息:

在这里插入图片描述

2.启动redis:

在这里插入图片描述

3.修复文件:使用redis-check-aof命令

在这里插入图片描述

4.查看文件:文件中手动添加的内容被清除,redis启动成功且数据恢复正常。

AOF同步频率设置:

redis.conf配置文件中:

# appendfsync always
appendfsync everysec
# appendfsync no

  1. 总是同步:即每次写入操作都会持久化记录(数据完成性高,性能差)
  2. 每秒钟同步一次
  3. 从不主动同步,将同步时机交给操作系统

Rewrite压缩:

重写压缩操作:对于多条命令,AOF最终都会将他压缩为尽可能少的命令(只关注结果,不关注过程),以减少日志文件的大小,如下:

set k1 v1
set k2 v2 
# 压缩为
set k1 v1 k2 v2

当文件达到一定大小时,才会进行重写压缩机制

也是使用fork来实现(写时复制技术)

3.比较

  • 推荐两者同时启用
  • 如果对数据的完整性要求不高,可单独使用RDB
  • 如果只做缓存,两者都不使用

9.主从复制

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

优点:

  1. 读写分离(主机做读操作,备用机做写操作)
  2. 快速的容灾恢复(从机宕机后,可以快速切换到其他的备用机)

1.配置

1.创建配置文件

在这里插入图片描述

内容如下(三个文件都是这样,只需修改端口号即可):

include ~/redis/MyRedis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

2.分别根据这三个配置文件启动redis

在这里插入图片描述

启动命令:redis-cli -p port

3.查看当前的角色

使用命令:info replication

在这里插入图片描述

由于当前没有任何配置,所以三台都是主机(master)

4.设置从机

使用命令:slaveof 主机ip 主机端口号

在这里插入图片描述

再次查看角色:

在这里插入图片描述

在这里插入图片描述

此时6380已经作为6379的从机了。

测试:

在6379(主机)中写入数据,在6380、6381(从集中可以读到该数据)

注意:

1.主机可以进行读、写操作

2.从机不能进行写操作

2.主从机宕机问题

1.从机断开连接重连

环境:一台主机两台从机。

当一台从机宕机后(shutdown),再重连有以下注意点:

  1. 一台从机宕机后,主机中显示只剩一台从机
  2. 宕机的服务重启后,是作为一台单独的主机(不会自动加入到主服务器的从机中)
  3. 使用命令重新连接后,会自动复制主机中的全部数据(即使在断开连接过程中,主机有添加新的数据)

2.主机断开连接及重连

环境:一台主机两台从机

当主机宕机、重连后,有以下几点:

  1. 主机断开连接后,从机不做任何变化(从机的主机还是原来的主机,但是将主机的状态显示为掉线状态)
  2. 主机重连后,依旧是主机,具有原来的从机;从机将自己的主机状态改为在线

3.薪火相传

在这里插入图片描述

从服务器可以把其他的从服务器作为自己的主服务器

缺点:当从服务1宕机时,其下面的从服务器也都无法操作。

命令:

  • 6380:slaveof 127.0.0.1 6379
  • 6381:slaveof 127.0.0.1 6380

此时,6381的 info replication显示6380为其主服务器

4.反客为主

当主服务器宕机后,从服务器会自动晋升为主服务器。

使用命令:slaveof no one

缺点:需要手动输入命令才能完成

5.主从复制原理

  1. 当从服务器连接到主服务器后,会主动发送一个同步数据请求
  2. 主服务器接收到同步请求后会将自己的数据进行持久化,然后将持久化文件(dump.rdb)发送给从服务器,从而实现同步
  3. 当主服务进行写操作后,会将新的数据发送给从服务器,以保证数据的同步。

6.哨兵模式

哨兵模式是反客为主的自动版,能够后台监控主机是否出现故障,如果出故障了,将根据投票数自动将从服务器转换为主服务器。

配置:

  1. 准备配置文件:sentinel.conf

    sentinel monitor mymaster 127.0.0.1 6380 1
    

测试

  1. 依次启动:master >>> slave(主服务器都为6379) >>> sentinel

    # sentinel 启动命令
    redis-sentinel ~/redis/MyRedis/sentinel.conf
    

    在这里插入图片描述

    即可看到哨兵配置完成,监视对象为6379,从机为6380、8381

  2. 关闭主服务器(6379)

    哨兵显示:(将主服务器切换为6380)

    在这里插入图片描述

    6380状态:(6380变为主服务器,且有从服务器一个6381)

    在这里插入图片描述

  3. 启动6379:

    6379变为6380的从服务器:

    在这里插入图片描述

    查看sentinel.conf配置文件:(主服务器变为6380)

    在这里插入图片描述

    在这里插入图片描述

选举规则:

  1. 选择优先级靠前的

    在配置文件中有:(值越小,优先级越高)

    replica-priority 100
    
  2. 选择偏移量大的(与原主服务器同步程度最高的从服务器)

  3. 选择runid最小的从服务(redis实例启动后,会随机产生一个40位的runid)

7.java中的主从复制

10.集群

Redis中的集群采用无中心化集群:即不使用代理服务器入口;而是任何一个模块的主服务器都可以作为入口。根据服务的类型判断该操作是否属于当前模块的功能操作,如果不是就会将该搞作转移给其他的主服务器操作。

在这里插入图片描述

集群的优点:

  • 水平扩大redis容量
  • 分摊压力
  • 无中心配置相对简单

集群的缺点:

  • 多建操作不被支持
  • 多建的Redis事务不被支持;lua脚本不支持

1.集群配置

  • 环境准备(本地模拟六台服务器):6379、6380、6381、6389、6390、6391

  • 配置文件内容:(其他的类似)

    include /root/redis/MyRedis/redis.conf
    pidfile "/var/run/redis_6379.pid"
    port 6379
    dbfilename "dump6379.rdb"
    # 开启集群
    cluster-enabled yes
    # 集群配置文件名称
    cluster-config-file nodes-6379.conf
    # 集群节点连接超时时间(超时就会更换节点)
    cluster-node-timeout 15000
    
  • 启动全部服务器:(启动并生成集群配置文件)

    在这里插入图片描述

  • 开启集群:

    跳转到redis安装目录的src文件夹下:

    cd ~/redis/redis-6.2.3/src
    

    使用命令:

    ./redis-cli --cluster create --cluster-replicas 1 192.168.1.119:6379 192.168.1.119:6380 192.168.1.119:6381 192.168.1.119:6389 192.168.1.119:6390 192.168.1.119:6391
    

    其中:1表示最简单的集群方式(3主3从);IP必须使用真实IP

    在这里插入图片描述

    输入yes表示接受该分配方式。

    在这里插入图片描述

  • 以集群方式连接:

    ./redis-cli -c -p 6379
    
  • 查看集群信息:

    cluster nodes
    

    在这里插入图片描述

2.分配规则

  • 一个居群至少有三台主机(master)
  • 尽量保证每个主机都不在同一个IP地址
  • 尽量保证每个主机与其从机不再统一个IP

这样确保一个IP的失效,其他的还能工作。

slots(插槽):

在启动集群后,会显示 16384 slots :表示该集群一共有16384个插槽,每一个插槽可对应一个key。

在存入数据时,根据key计算(CRC(key)%16384)其属于哪个插槽。

如上集群:

  • 主机6379:0-5460
  • 主机6380:5461-10922
  • 主机6381:10923-16383

如:添加一个数据,显示下面的信息

在这里插入图片描述

多key-value添加:

由于多数据同时添加,无法计算对应的插槽值,所以需要使用来添加:

mset key1{组名} value key2{组名} value key3{组名} value

在这里插入图片描述

其他操作:

  • 查询key对应的slot值:

    cluster keyslot String
    

    在这里插入图片描述

  • 查询某个插槽内的key的数量:

    cluster countkeysinslot slot
    

    在这里插入图片描述

    只能查看自己插槽范围内的

  • 查询某个插槽内的key:

    cluster getkeysinslot slot max
    

    在这里插入图片描述

故障恢复:

当一台主机宕机后,其从机(slave)会自动变为主机(master),而当原来的主机重新启动后,就会变为新的主机的从机。注意:需要超时15秒

测试:关闭主机6379(其从机为6391)

在这里插入图片描述

看到6391变为主机,6379关闭。

启动6379:

在这里插入图片描述

看到6379变为6391的从机。

主从都宕机:如果某一块的主机和从机都宕机,有如下两种情况:

在redis.conf配置文件中,有如下配置:

cluster-require-full-coverage yes
  • 如果是yes,则表示当一组主从宕机后,全部集群都不再工作
  • 如果是no,则表示当一组主从宕机后,则该组不再工作

3.java操作Redis集群

public class demo1 {
    public static void main(String[] args) {
        HostAndPort hostAndPort = new HostAndPort("192.168.1.119", 6391);
        JedisCluster jedisCluster = new JedisCluster(hostAndPort);
        jedisCluster.set("test", "cluster-java");
        String test = jedisCluster.get("test");
        System.out.println(test);
    }
}

需要注意的是:如果当前使用的IP+端口的服务关闭了,则会调用失败;所以可以在JedisCluster中传入一个Set(包含集群中的多个服务器地址)

上面的方法是直接使用Jedis,下面是使用springboot+redistemplate

只需要在配置文件中配置:

spring:
  redis:
    # redis 服务器地址
    host: 192.168.1.119
    # 服务器端口号
    port: 6379
    # 数据库索引(默认为0)
    database: 0
    # 连接超时时间
    timeout: 1800000

    lettuce:
      pool:
        # 连接池最大连接数,负值表示没有限制
        max-active: 20
        # 最大空间连接
        max-idle: 5
        # 最小空闲连接
        min-idle: 0
        # 最大阻塞等待时间
        max-wait: -1
    # 集群配置
    cluster:
      nodes: 192.168.1.119:6379,192.168.1.119:6380,192.168.1.119:6381,192.168.1.119:6389,192.168.1.119:6390,192.168.1.119:6391

配置完成后即可直接使用redisTemplate访问

11.应用问题

1.缓存穿透

简单理解:当Redis作为缓存时,在正常情况下,用户访问web服务器请求数据可以从redis缓存中直接获取,少量缓存中没有的数据在向数据库(Mysql等)中查询。但是,当访问量突然增大,Redis的命中率降低(用户的请求数据不能从缓存中直接获取),就需要大量的访问数据库,就有可能导致数据库崩溃,而此时Redis依然正常运行。即缓存穿透。

在这里插入图片描述

可能造成的行为:

  • Redis无法查询到数据
  • 大量的非正常访问

解决方案:

  1. **对空值缓存:**当一个查询的结果为空时,任然将其添加到缓存(null),但是该空结果的过期时间会很短,最长5分钟。
  2. **设置可访问的名单(白名单):**使用bitmaps类型数据定义一个可访问名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,只有在里面的id才能访问。缺点:每次都要查询bitmps,效率低。
  3. **使用布隆过滤:**其底层原理和bitmaps类似,但是对其进行了优化,便于快速查询。缺点:命中率低。
  4. **进行实时监控:**当发现Redis的命中率急剧下降时,需要排查访问对象和访问的数据,可以设置黑名单对其限制。

2.缓存击穿

简单理解:当Redis中没有出现大量key过期且Redis运行正常,而数据库的访问压力瞬间增大,则会造成缓存击穿。

可能造成的行为:

  • 由于redis中的某个热门数据过期而导致数据库访问量大大增加。

解决:

  1. 预先设置热门数据:在访问高峰之前,把热门数据加入到redis中,并加大时长。
  2. 实时调整:现场监控哪些数据时热门的,适时调整其key的过期时长。
  3. 使用锁:当redis查询到的结果为空时,将其锁起来,不允许访问;过一段时间再访问,如果返回不为空了,就将其解锁。缺点:效率低下。

缓存穿透与缓存击穿的区别:

  • 首先:缓存穿透是由于Redis中本身就不存在某个key而导致数据库访问量大大增加;而缓存击穿是由于Redis中原本有该key,而现在这个key过期了导致的。
  • 其次是造成原因:缓存穿透较大可能是由于黑客攻击;而缓存击穿是由于key的过期所导致。

3.缓存雪崩

简单理解:缓存雪崩是由于在短期内,由于大量的key过期(缓存失效),导致服务器大量的访问数据库导(底层压力骤加),致服务器瘫痪,造成雪崩。

解决:

  1. **构建多级缓存架构:**nginx+redis+其他缓存
  2. **使用锁或队列:**有类似于阻塞的情况,效率低,比适用于高并发场景。
  3. **设置过期标志更新缓存:**记录缓存数据是否过期或将要过期,通知其他线程在后台更新过期时间。
  4. **将缓存失效时间分散开:**即错开每个可以的过期时间。

4.分布式锁

在分布式(或集群)中,由于每个服务或服务器所在的地址不同,所以在使用锁得时候,只对单体有效,而其他的服务中不受此锁的约束,所以需要解决这个问题,让一把锁在全局能够有效。

锁应具有的特性:

  1. 互斥性:任何时刻,只能有一个客户端能有拥有锁
  2. 没有死锁:即保证一个客户端在没有主动解锁的情况下也能够自动解锁,保证其他客户端能够使用(设置过期时间)
  3. 加锁和解锁的必须时同一个客户端
  4. 加锁和解锁必须具有原子性

实现方式:

  1. 基于数据库实现分布式锁(alibaba的seata在在数据库底层使用了行锁,来控制全局事务,可以借鉴其思想)
  2. 基于Redis(性能高)
  3. 基于Zookeeper(用于分布式,常作为服务注册中心,可靠性最高)

在Redis中的实现:

**1.**使用命令setnx key value即为该数据上锁

使用命令del key即为该数据解锁

解释:当时用setnx后,如果该key存在,则不能再添加该key的数据,只有当删除这个key之后才能添加。

缺点:必须要先删除才能添加

**2.**为解决上面的问题,可以为该数据添加过期时间expire key seconds

当过期时间到了之后,就会自动解锁,即可添加新的数据。

缺点:如果再添加值后突然出现异常,导致没有设置过期时间则无法解锁。

**3.**为解决2的问题,可使用如下命令完成添加数据的同时加锁

set key value nx ex seconds

nx:表示加锁;ex:表示设定过期时间

java代码演示:

@RestController
public class LockController {
    @Resource(name = "redisTemplate1")
    private RedisTemplate redisTemplate;
    
    @GetMapping("/test/lock")
    public String testLock(){
        // 上锁,即setnx命令
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
        // 使用  set key value nx ex seconds 命令
//      Boolean lock1 = redisTemplate.opsForValue().setIfAbsent("lock", "lock",1, TimeUnit.SECONDS);
        // 当上锁成功(表明之前时解锁状态)
        if (lock){
            // 获取值
            Object val = redisTemplate.opsForValue().get("number");
            // 判断是否为空
            if(val == null){
                System.out.println("key不存在");
                return "key不存在";
            }
            // +1并存入
            int num = Integer.valueOf(String.valueOf(val));
            redisTemplate.opsForValue().set("number", ++num);
            // 解锁
            redisTemplate.delete("lock");
            return "success";
        }
        // 上锁失败(当前锁被其他使用)
        return "is locking";
    }

}

上述代码演示了通过setnx命令来实现上锁机制,再实际使用中,使用同一个锁(key名称相同)即可实现分布式锁的全局性

上述代码依然存在为题:

在这里插入图片描述

@RestController
public class LockController {
    @Resource(name = "redisTemplate1")
    private RedisTemplate redisTemplate;

    @GetMapping("/test/lock")
    public String testLock(){
        // 使用UUID作为lock的值
        String uuid = UUID.randomUUID().toString();
        // 上锁
        // 使用  set key value nx ex seconds 命令
        Boolean lock1 = redisTemplate.opsForValue().setIfAbsent("lock", "lock",1, TimeUnit.SECONDS);
        // 当上锁成功(表明之前时解锁状态)
        if (lock){
            // 获取值
            Object val = redisTemplate.opsForValue().get("number");
            // 判断是否为空
            if(val == null){
                System.out.println("key不存在");
                return "key不存在";
            }
            // +1并存入
            int num = Integer.valueOf(String.valueOf(val));
            redisTemplate.opsForValue().set("number", ++num);
            // 解锁
            // 在UUID相同的时候才能解锁
            Object uuidFromRedis = redisTemplate.opsForValue().get("lock");
            if(uuid.equals(String.valueOf(uuidFromRedis))){
                redisTemplate.delete("lock");
            }
            return "success";
        }
        // 上锁失败(当前锁被其他使用)
        return "is locking";
    }

}

还有问题:

缺乏原子性。

  • 在A操作的过程中,到了正要删除锁的时候(可以极端的理解为:uuid已近比较相等,但还没有执行删除这个命令),由于过期时间到了,自动释放了锁;
  • 此时B操作就可以获得锁,并操作;
  • 而后,A操作就释放了B的锁
  • 需要结合代码理解(这是在很短的一段时间内)

解决:

使用lua脚本,该脚本时嵌入式脚本,不允许被打断,所以保证了原子性。

12.Redis6新功能

1.ACL 访问控制列表

官网:https://redis.io/commands

  • 查看全部用户列表

    acl list
    
  • 查看当前用户

    acl whoami
    
  • 查看命令集集

    acl cat
    # 查看具体命令
    acl cat string
    acl cat list
    
  • 创建用户

    acl setuser 用户名 on >密码 ~* +@all"
    
  • 切换用户

    auth 用户名 密码
    

2.IO多线程

Redis6中依然是单线程+IO多路复用

这里的多线程只是redis对网络数据和协议处理的多线程,而执行命令依然是单线程。

3.cluster工具

在redis6中,集成了Ruby环境,不需要再安装该环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值