Featured image of post Linux Redis的使用

Linux Redis的使用

🌏Linux_Redis的使用 🎯 这篇文章用于记录Linux_Redis的使用 【环境说明】CentOS Redis-6.2.6

🎄说明

基于CentOS 7.6下常用软件的安装 记录的Redis的安装 之后 再来看看如何使用安装后的Redis

需要注意的是在CentOS 7.6下常用软件的安装文章中用的是root用户进行安装的 但经过在后面的redis的使用的踩坑的经历中 发现最好用一个新的用户来管理redis 而不是root用户 所以我创建了一个新的redis用户 并在redis用户下重新编译安装redis 并且在之后的redis使用中 都需要切换到redis用户来进行

🎄Redis的配置

🍭获取所有的配置项CONFIG GET *

修改/export/server/redis/redis.conf 配置文件

🍭如果要向远程访问服务器上的Redis 则需要将bind 127.0.0.1 -::1注释掉 同时关闭保护模式 否则无法远程访问 当然一般在开发测试的时候可能会远程访问 所以将其开启

🍭timeout的设置是指当server-cli开启后多久自动关闭 很显然我们并不希望它关闭 所以无需修改

🍭tcp-keepalive 是对访问客户端的一种心跳检测 每隔 n 秒检测一次 单位为秒

🍭daemoize yes表示支持后台启动 如果设置为noredis会随着窗口的关闭而关闭

🍭存放 pid 文件的位置 每个实例会产生一个不同的 pid 文件 记录 redis 的进程号

🍭redis 日志分为 4 个级别,默认的设置为 notice, 开发测试阶段可以用 debug(日志内容较多,不建议生产环境使用),生产模式一般选用 notice

🍭如果提示日志文件 redis.log 不存在,创建一个该文件即可

🍭设置数据库数量

🍭是否启用密码(可以联想mysql -uroot -p 会要求输入密码 这里的配置是一个意思)目前不需要 当前 如果服务器暴露开启了Redis端口 则需要考虑开启

🍭设置 redis 同时可以与多少个客户端进行连接 默认情况下为 10000 个客户端 如果达到了此限制,redis会拒绝新的连接请求,并且向这些连接请求方发出"max number of clients reached"

🍭限制redis占用系统的内存 在默认情况下, 对 32 位 实例会限制在 3 GB, 因为 32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃 在默认情况下, 对于 64 位实例是没有限制 当用户开启了 redis.conf 配置文件的maxmemory选项,那么Redis将限制选项的值不能小于1 MB

设置建议 [1] Redis 的 maxmemory 设置取决于使用情况, 有些网站只需要 32MB,有些可能需要 12GB [2] maxmemory 只能根据具体的生产环境来调试,不要预设一个定值,从小到大测试,基本标准是不干扰正常程序的运行。[3] Redis 的最大使用内存跟搭配方式有关,如果只是用 Redis 做纯缓存, 64-128M 对一般小型网站就足够了 [4] 如果使用 Redis 做数据库的话,设置到物理内存的 1/2 到 3/4 左右都可以 [5] 如果使用了快照功能的话,最好用到 50%以下,因为快照复制更新需要双倍内存空间,如果没有使用快照而设置 redis 缓存数据库,可以用到内存的 80%左右,只要能保证 Java、NGINX 等其它程序可以正常运行就行了

🎄Redis的数据类型和应用场景

🙂Redis是一个超大的HashMap<KEY, VAL> 说Redis的数据类型 其实是指它的VAL 也就是值的数据类型

Redis常用的数据类型有以下几种:

🍭String字符串类型

字符串是Redis最基础的数据类型,可以存储任何数据(最大可存储512M),包括数字、字符串、二进制数据、序列化的对象等 常见的业务场景包括:

  • 存储简单的配置信息
  • 存储用户信息 -> 序列化
  • 存储访问令牌(Token)等认证信息

🍭List列表类型

列表是一种简单的线性数据结构, 底层使用双向链表存储结构, 可以存储多个有序的字符串元素。正因为是双向链表结构,所以可将元素插入到链表的头部或者尾部。

常见的业务场景包括:

  • 系统通知(BiliBili、社区项目的系统通知 最近的通知列在最前面 最新关注等)
  • 存储一系列任务或事件,如待办事项、日志记录等

🍭Hash散列类型

哈希数据类型用于存储键值对集合,其中每个键都映射到一个字段,每个字段都映射到一个值。常见的业务场景包括:

  • 存储用户详细信息,如姓名、年龄、邮箱等
  • 存储对象的状态信息,如订单状态、商品信息等

🍭Set集合类型

集合是一种无序的数据结构,可以存储多个不重复的字符串元素。常见的业务场景包括:

  • 实现标签系统,如用户标签、分类等
  • 存储唯一标识符,如访问用户的唯一ID、设备ID等

🍭Sorted_Set集合类型

有序集合与集合类似,但每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的元素进行从小到大的排序。常见的业务场景包括:

  • 实现评分系统,如商品评分、文章评分等
  • 存储用户积分排行榜,根据积分对用户进行排序

🎄Redis的持久化机制

概述 Redis的持久化有两种方式 1️⃣RDB(Redis DataBase)2️⃣AOF(Append Only File)

🎯RDB(Redis DataBase)

🍭RDB 是什么

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

🍭RDB 的流程

  1. redis 客户端执行 bgsave 命令或者自动触发 bgsave 命令;
  2. 主进程判断当前是否已经存在正在执行的save子进程(进行持久化数据),如果存在,那么主进程直接返回;
  3. 如果不存在正在执行的save子进程(进行持久化数据),那么就 fork 一个新的子进程进行持久化数据,fork 过程是阻塞的,fork 操作完成后主进程即可执行其他操作;
  4. 子进程先将数据写入到临时的 rdb 文件中,待快照数据写入完成后再原子替换旧的 rdb文件;
  5. 同时发送信号给主进程,通知主进程 rdb 持久化完成,主进程更新相关的统计信息4

🍭RDB 的特点

  • 整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能
  • 如果需要进行大规模数据的恢复, 且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效
  • RDB 的缺点是最后一次持久化后的数据可能丢失(如果是正常关闭 Redis , 仍然会进行持久化, 不会造成数据丢失;如果是 Redis 异常终止/宕机, 就可能造成数据丢失)

🍭RDB 的配置

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

其位置默认为 Redis 启动时命令行所在的目录下(如有需要 可以将其修改为固定的目录)

由于我之前在红色框的目录(/export/server/redis)下启动Redis 所以dump.rdb文件在/export/server/redis/目录下

🍭save vs bgsave

1、save 时只管保存,其它不管,全部阻塞。手动保存, 不建议。

2、bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

3、可以通过 lastsave 命令获取最后一次成功执行快照的时间(unix 时间戳) , 可以使用工

具转换

🍭stop-writes-on-bgsave-error

当 Redis 无法写入磁盘的话(比如磁盘满了), 直接关掉 Redis 的写操作 推荐设置为 yes

🍭rdbcompression

对于存储到磁盘中的快照,可以设置是否进行压缩存储 如果是的话,redis 会采用LZF 算法进行压缩 如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能 默认 yes

🍭RDB 备份&恢复

Redis 可以充当缓存, 对项目进行优化, 因此重要/敏感的数据建议在 Mysql要保存一份 从设计层面来说, Redis 的内存数据, 都是可以重新获取的(可能来自程序, 也可能来自Mysql) 因此这里说的备份&恢复主要是指 Redis 启动时, 初始化数据是从dump.rdb 来的

将 dump.rdb 进行备份, 如果有必要可以写 shell 脚本来定时备份

🍭RDB 持久化小结总结

RDB的优势

1、适合大规模的数据恢复

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

3、节省磁盘空间

4、恢复速度快

RDB的不足

1、虽然 Redis 在 fork 时使用了写时拷贝技术(Copy-On-Write), 但是如果数据庞大时还是比较消耗性能

2、备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话(如果正常关闭 Redis, 仍然会进行 RDB 备份, 不会丢失数据), 就会丢失最后一次快照后的所有修改

🎯AOF(Append Only File)

🍭AOF 是什么?

以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来 (比如 set/del 操作会记录, 读操作 get 不记录) 只许追加文件但不可以改写文件

redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

🍭AOF 持久化小结总结

优势:

(1)备份机制更稳健,丢失数据的概率更低

(2)可读的日志文本,通过操作AOF稳健,可以处理误操作

劣势:

(1)比RDB占用更多的磁盘空间

(2)恢复备份速度相对慢

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

🎄Redis事务场景

🍭Redis的事务特性

(1)Multi序列化所有命令 Exec事务中的所有命令都会序列化、按顺序地执行 事务在执行的过程中,不会被其他客户端发送来的命令请求所打断 而在组队命令的时候可以使用discard命令取消组队(相当于MySQL的Rollback命令) 需要注意的是,如果组队阶段报错,则会导致exec失败,那么事务的所有指令都不会执行

(2)无隔离级别的概念

(3)不保证原子性(事务执行过程中 如果有指令执行失败 其它的指令仍然会被执行 没有回滚)

🍭悲观锁和乐观锁

🎯悲观锁

(1)悲观锁,顾名思义,某个都认为别人可能会修改,所以 如此一来,其他请求线程想要取这个数据的时候就会被阻塞,直到其拿到锁才能对这个数据进行访问

(2)悲观锁是锁设计理念,传统的关系型数据库里面就用到了很多这种锁的机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁

🎯乐观锁

(1)乐观锁,顾名思义,每次去取数据的时候,都先假定没有其他线程的并发访问,所以不会上锁 但是在更新的时候会判断在此期间有没有其他线程来更新了这个数据,可以使用版本号的机制来判断

(2)乐观锁适用于读操作较多的场景,这样可以提高吞吐量 Redis就是利用这种check-and-set机制来实现事务的

(3)根据以上机制 我们可以想到 [](乐观锁在进行版本号的修改的时候 需要是原子性操作)

🍭利用Redis事务机制(乐观锁) 解决非高并发 超卖

🌴问题复现

用户单个请求进行购买 可以完成

/**
 * 购票非高并发的情况 到 高并发的情况
 *
 * @param uid      用户id - 在后台生成
 * @param ticketNo 票的编号, 比如北京-成都的ticketNo 就是bj_cd
 */
public static boolean doSecKill(String uid, String ticketNo) {

    // uid 和 ticketNo进行非空校验

    //- 通过连接池获取到jedis对象/连接(优化获取连接的时间)
    JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
    Jedis jedis = jedisPoolInstance.getResource();
    System.out.println("---使用的连接池技术----");

    //- 拼接票的库存key
    String stockKey = "sk:" + ticketNo + ":ticket";
    //- 拼接秒杀用户要存放到的set集合对应的key 这个set集合可以存放多个userId
    String userKey = "sk:" + ticketNo + ":user";

    /**
     * (1) 检查对应的票是否存在
     * (2) 检查用户是否重复购票
     * (3) 检查火车票是否还有剩余
     */

    /* 不使用Transaction事务进行处理的时候 两步操作单独分步执行*/
    jedis.decr(stockKey); // 1. 将票的库存量-1
    jedis.sadd(userKey, uid); // 2. 将该用户加入到抢购成功对应的set集合中

    System.out.println(uid + " 秒杀成功..");
    jedis.close();
    return true;
}

用户单个请求进行购买 可以完成 但是高并发测试会出现超卖 使用ab并发工具测试:

ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded
http://192.168.198.1:8080/seckill/secKillServlet

(1) ~/postfile文件下存储请求要携带的参数
(2) -T application/x-www-form-urlencoded 表示以表单的方式提交
(3) -n 1000 -c 100 共1000个请求每次发送100个10次发完

执行测试:

查看执行结果(超卖):

🌴问题解决

使用Redis事务

/**
 * 购票非高并发的情况 到 高并发的情况
 *
 * @param uid      用户id - 在后台生成
 * @param ticketNo 票的编号, 比如北京-成都的ticketNo 就是bj_cd
 */
public static boolean doSecKill(String uid, String ticketNo) {

    // uid 和 ticketNo进行非空校验

    //- 通过连接池获取到jedis对象/连接(优化获取连接的时间)
    JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
    Jedis jedis = jedisPoolInstance.getResource();
    System.out.println("---使用的连接池技术----");

    //- 拼接票的库存key
    String stockKey = "sk:" + ticketNo + ":ticket";
    //- 拼接秒杀用户要存放到的set集合对应的key 这个set集合可以存放多个userId
    String userKey = "sk:" + ticketNo + ":user";

    // 监控库存 注意这一步不能少 事务执行过程中如果库存变更 则事务会执行失败
    jedis.watch(stockKey);
    
    /**
     * (1) 检查对应的票是否存在
     * (2) 检查用户是否重复购票
     * (3) 检查火车票是否还有剩余
     */
    
    //使用事务,完成秒杀
    Transaction multi = jedis.multi();

    //组队操作
    multi.decr(stockKey);//减去票的库存
    multi.sadd(userKey, uid);//将该用户加入到抢购成功对应的set集合中

    //执行
    List<Object> results = multi.exec();

    if(results == null || results.size() == 0) {
        System.out.println("抢票失败...");
        jedis.close();
        return false;
    }

    System.out.println(uid + " 秒杀成功..");
    jedis.close();
    return true;
}

测试未出现超卖:

🍭使用lua脚本解决高并发 库存遗留问题

🌴问题复现 高并发库存遗留

ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded
http://192.168.98.1:8080/seckill/secKillServlet

(1) 库存设大一些 为600
(2) 并发量大一些 为300

执行后出现库存遗留:

🌴问题解决 使用Redis的分布式锁机制

处理秒杀请求的Servlet -> 调用 SecKillRedisByLua.doSecKill(userId, ticketNo)

/**
 * 处理秒杀请求的Servlet
 */
public class SecKillServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 请求时,模拟生成一个userId
        String userId = new Random().nextInt(10000) + "";
        //2. 获取用户要购买的票的编号
        String ticketNo = request.getParameter("ticketNo");
        //3. 调用秒杀的方法
        //boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);
        //3. 调用lua脚本完成秒杀方法
        boolean isOk = SecKillRedisByLua.doSecKill(userId, ticketNo);
        //4. 将结果返回给前端-这个地方可以根据业务需要调整
        response.getWriter().print(isOk);
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}

使用Lua脚本完成秒杀

/**
 * 使用Lua脚本完成秒杀
 */
public class SecKillRedisByLua {
    /**
     * 1. 这个脚本字符串是在lua脚本上修改的, 但是不完全是字符串处理
     * 2. 比如 : 这里就使用了 \" , 还有换行使用了 \r\n
     * 3. 如果直接把lua脚本粘贴过来是不可行的
     */
    static String secKillScript = "local userid=KEYS[1];\r\n" +
            "local ticketno=KEYS[2];\r\n" +
            "local stockKey='sk:'..ticketno..\":ticket\";\r\n" +
            "local usersKey='sk:'..ticketno..\":user\";\r\n" +
            "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
            "if tonumber(userExists)==1 then \r\n" +
            "   return 2;\r\n" +
            "end\r\n" +
            "local num= redis.call(\"get\" ,stockKey);\r\n" +
            "if tonumber(num)<=0 then \r\n" +
            "   return 0;\r\n" +
            "else \r\n" +
            "   redis.call(\"decr\",stockKey);\r\n" +
            "   redis.call(\"sadd\",usersKey,userid);\r\n" +
            "end\r\n" +
            "return 1";

    //使用lua脚本完成秒杀的核心方法
    public static boolean doSecKill(String uid,String ticketNo) {
        //先从redis连接池,获取连接
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();
        //就是将lua脚本进行加载
        String sha1 = jedis.scriptLoad(secKillScript);
        /*
         (1) Object result:这是方法的返回类型。这意味着jedis.evalsha方法执行后,返回的结果将被存储在名为result的变量中。
             具体返回什么起决于lua脚本的返回值
         (2) jedis.evalsha:这是Jedis库中的一个方法,用于执行Redis的EVALSHA命令。EVALSHA命令用于执行存储在Redis中的Lua脚本。
         (3) sha1:这是evalsha方法的第一个参数,它是一个字符串,表示要执行的Lua脚本的SHA-1校验和。SHA-1是一种散列算法,用于将数据(在这种情况下是Lua脚本)转换为固定长度的唯一标识符。通过这个SHA-1值,Redis可以在其脚本缓存中快速查找并执行相应的脚本。
         (4) 2:这是evalsha方法的第二个参数,表示传递给Lua脚本的参数的数量。在这个例子中,脚本将接收两个参数。
         (5) uid 和 ticketNo:这两个参数是具体的值,它们将被传递给Lua脚本作为输入。uid代表用户ID,而ticketNo代表票号
             这两个参数会在lua脚本中用到 -> local userid=KEYS[1]; local ticketno=KEYS[2]; lua脚本会使用KEYS关键字把变量值取出来使用
         */
        Object result = jedis.evalsha(sha1, 2, uid, ticketNo);
        String resString = String.valueOf(result);

        //根据lua脚本执行返回的结果,做相应的处理
        if("0".equals(resString)) {
            System.out.println("票已经卖光了..");
            jedis.close();
            return false;
        }
        if("2".equals(resString)) {
            System.out.println("不能重复购买..");
            jedis.close();
            return false;
        }
        if("1".equals(resString)) {
            System.out.println("抢购成功");
            jedis.close();
            return true;
        } else {
            System.out.println("购票失败..");
            jedis.close();
            return false;
        }
    }
}

测试未出现票数遗留问题:

❓ lua脚本为什么能够解决高并发库存遗留问题?

(1)lua脚本类似Redis的事务,具有原子性,不会被其他命令插队

(2)通过lua脚本解决争抢问题,Redis利用其单线程的特性,将请求形成任务队列,从而解决多线程的并发问题

🎄Redis分布式锁的使用

@RequestMapping("/seckill/{path}/addOneAndReturnInfo")
@ResponseBody
public Msg addOneAndReturnInfo(@RequestParam("userId") String userId,
                               @RequestParam("commId") Integer commId,
                               @RequestParam(value = "originPrice", required = false) Double originPrice,
                               @RequestParam(value = "seckillPrice",required = false) BigDecimal seckillPrice,
                               @RequestParam(value = "pic",required = false) String pic,
                               @PathVariable(value = "path",required = false) String path,
                               Model model) {
    System.out.println("C SeckillController M addOneAndReturnInfo()..");

    Msg msg = new Msg();

    log.info("判断秒杀路径是否正确");
    /****** 0 判断秒杀路径是否正确 ******/
    String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + userId + ":" + commId);
    if (!StringUtils.hasText(path) || !redisPath.equals(path)) {
        msg.setCode(408);
        msg.setMsg("秒杀路径出错 请稍后重试~");
        //return "secKillFail";
        return msg;
    }

    //log.info("查询库存是否够用(内存优化)emptyStockMap");
    /****** 1.1 先查询库存是否够用(内存优化) ******/
    Boolean ifEmpty = emptyStockMap.get(commId);
    if(ifEmpty != null && ifEmpty) {
        msg.setCode(401);
        msg.setMsg("该商品已售罄~");
        return msg;
    }

    /**
     *  2 再查询用户是否重复购买
     *
     *  这个判断方法可以进一步改进 例如在所有的秒杀商品中增加一个字段
     *  用于标识不同活动 在判断用户是否复购时增加这个字段的判断
     *
     *  直接从redis中取用户的购买记录信息而不是MySQL数据库 优化查询效率
     */
    log.info("查询用户是否重复购买");
    Integer purchaseRecord = (Integer)redisTemplate.opsForValue().get("purchased" + ":" + commId + ":" + userId);
    if(purchaseRecord != null){
        msg.setCode(403);
        msg.setMsg("您已经购买过此商品 不能重复购买~");
        return msg;
    }

    // 获取分布式锁
    log.info("获取分布式锁");
    String uuid = UUID.randomUUID().toString();
    /**
     * 尝试设置一个键值对,其中键为"lock",值为uuid,并且设置了过期时间为3秒
     *
     * 如果执行失败 redisTemplate.opsForValue().setIfAbsent()方法会返回一个false
     * 这个false布尔值表示操作未能成功执行 即键"lock"未能被成功设置
     */
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    // 如果锁未成功设置 则轮询继续获取这个锁 循环获取分布式锁
    while(lock == null){
        lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    }
    // 只有一个线程能执行if语句中的代码
    if(lock) {
        // 释放锁准备 为了防止误删除其它用户的锁 先判断当前的锁是不是前面获取到的锁 如果相同 再释放
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
            "then return redis.call('del', KEYS[1]) " +
            "else return 0 " +
            "end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);

        // 从redis获取商品的库存并判断 然后预减库存
        Integer stock = (Integer)redisTemplate.opsForValue().get("seckillGoods:" + commId);

        if(stock == 0) {
            emptyStockMap.put(commId, true); // 此用户购买完成后存储就为空了
            msg.setCode(401);
            msg.setMsg("该商品已售罄~");
            return msg;
        } else {
            // 库存预减
            log.info("库存预减");
            redisTemplate.opsForValue().set("seckillGoods:" + commId, stock - 1);
        }
        /**
         * 释放分布式锁
         *
         * redisScript: 执行的lua脚本
         *
         */
        redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);

        /**
         * 释放锁之后 用 RabbitMQ 消息队列来处理最终的购买业务
         * 代码执行到这里 说明用户抢到了一个储存 只要后面的消息队列数据库操作成功 就购买成功
         */
        //Gson gson = new Gson();
        log.info("将秒杀操作交给消息队列");
        SeckillMessage seckillMessage = new SeckillMessage(userId, commId);
        mqSenderMessage.senderMessage(JSONUtil.toJsonStr(seckillMessage)); // 发出去之后 由Receiver进行处理
        msg.setCode(200);
        msg.setMsg("秒杀排队中,请稍后~");
    } else {
        msg.setCode(409);
        msg.setMsg("获取分布式锁失败,秒杀失败,请稍后重试~");
    }
    return msg;
}

🎄Redis的终端操作

模糊匹配删除redis的键

127.0.0.1:6379> EVAL “local keys = redis.call(‘KEYS’, ‘user133’) for _,key in ipairs(keys) do redis.call(‘DEL’, key) end” 0 127.0.0.1:6379> EVAL “local keys = redis.call(‘KEYS’, ‘purchased*’) for _,key in ipairs(keys) do redis.call(‘DEL’, key) end” 0

🎄Redis集群

🍭主从复制

🌸概述

(1)主节点更新数据后 自动同步到备机的master/slaver结点 主从复制要求1主多从 因为如果有多个master 那么slaver不能确定和哪个master进行同步,会出现数据紊乱

(2)Master以写为主 Slaver以读为主

(3)好处1:读写分离 提升效率 -> 读写分离后,将读操作和写操作分布到不同的Redis,减少单个Redis的压力,从而提升效率

(3)好处2:容灾快速恢复 -> 如果某个slaver不能正常工作了,可以切换到另一个slaver

(4)要解决主服务器的高可用性,可以使用Redis集群

🌸主从复制的流程

【1】Slaver启动成功 -> 在这个时候,Slaver会发送一个sync命令到Master结点,Master结点在接收到sync命令后将启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕之后,master将传送整个数据文件到Slaver,Slaver将此文件存盘并加载到内存中,以完成一次全量同步(全量复制)

【2】当Master的数据发生变更 -> 会将新的收集到的修改命令依次传送给slaver,完成增量同步(增量复制)

【3】如果从服务器down了 重新启动后 仍然可以执行一次全量同步 获取Master的最新数据

【4】如果主服务器down 了, 从服务器并不会抢占为主服务器, 当主服务器恢复后, 从服务仍然指向原来的主服务器

🍭薪火相传

(1)上一个slave可以是下一个slave的Master slave同样可以接收其他slaves的连接和同步请求 那么该slave作为了链接下一个结点的master 这样的集群部署方式可以有效减轻master的写压力 去中心化降低风险

(2)缺点:一旦某个slave宕机 后面的slave都没法同步 主机挂了 从机还是从机 没办法写数据了

🍭反客为主

在薪火相传的模式下,当一个master宕机后,指向master的slave可以升为master(用salveof no one将从机变为主机),其后面的slave不用做任何修改

🍭哨兵模式(sentinel) 反客为主的自动版

创建 /redis/sentinel.conf , 名字不能乱写, 按照指定的来

sentinel monitor redis_master 127.0.0.1 6379 1

-redis_master 为监控对象起的服务器名称
-1 表示至少有多少个哨兵同意迁移的数量, 这里配置1 表示只要有1个哨兵同意迁移就可以切换

启动哨兵, 注意看哨兵的端口是 26379

当主机挂掉,从机选举中产生新的主机

如果原来的主机重启,会自动成为从机

❓哨兵如何在从机中, 推选新的Master主机?

选择的条件依次为:

  • 优先级在 redis.conf 中默认:replica-priority 100,值越小优先级越高

  • 偏移量是指获得原主机数据的量, 数据量最全的优先级高

  • 每个 redis 实例启动后都会随机生成一个 40 位的 runid, 值越小优先级越高

sentinel.conf配置文件的使用方法?

sentinel.conf配置文件是用于配置Redis Sentinel哨兵节点的关键文件。以下是sentinel.conf配置文件的配置方法和使用方法的详细说明,并通过两个典型的例子进行说明:

配置方法:

  1. sentinel monitor:用于指定要监视的主节点的名称、IP地址、端口号和哨兵节点的数量。语法如下:
sentinel monitor <master-name> <ip> <port> <quorum>

其中,<master-name>是主节点的名称,<ip><port>是主节点的IP地址和端口号,<quorum>是哨兵节点的数量。

  1. sentinel down-after-milliseconds:用于指定在多少毫秒后认为主节点不可用。语法如下:
sentinel down-after-milliseconds <master-name> <timeout>

其中,<master-name>是主节点的名称,<timeout>是超时时间(毫秒)。

  1. sentinel failover-timeout:用于指定在故障转移期间的最大超时时间。语法如下:
sentinel failover-timeout <master-name> <timeout>

其中,<master-name>是主节点的名称,<timeout>是超时时间(毫秒)。

  1. sentinel parallel-syncs:用于指定可以同时进行同步的哨兵节点的数量。语法如下:
sentinel parallel-syncs <master-name> <count>

其中,<master-name>是主节点的名称,<count>是同步的哨兵节点数量。

  1. 其他配置项:除了上述关键配置项外,sentinel.conf文件中还可以包含其他配置项,如日志设置、端口号等。

使用方法:

  1. 启动Sentinel:在配置好sentinel.conf文件后,需要启动Sentinel进程来监视主节点。你可以使用以下命令启动Sentinel进程:
redis-sentinel /path/to/sentinel.conf

其中,/path/to/sentinel.conf是你的sentinel.conf文件的路径。

  1. 故障转移:当主节点不可用时,Sentinel会自动进行故障转移操作,将一个从节点提升为新的主节点,并更新其他从节点的复制目标。故障转移过程是自动完成的,不需要手动干预。
  2. 管理命令:Redis Sentinel提供了一些管理命令,用于获取Sentinel的状态、主节点信息、从节点信息等。你可以使用以下命令来获取Sentinel的状态信息:
redis-cli -h <sentinel-ip> -p <sentinel-port> sentinel <command>

其中,<sentinel-ip><sentinel-port>是Sentinel的IP地址和端口号,<command>是要执行的管理命令,例如mastersslaves等。

  1. 配置更新:如果你需要更新Sentinel的配置,可以在不停止Sentinel进程的情况下重新加载配置文件。使用以下命令重新加载配置文件:
redis-sentinel /path/to/sentinel.conf reload

这将使Sentinel重新加载配置文件并应用新的配置。重新加载配置文件可能需要一些时间,具体取决于配置文件的规模和系统性能。