大数据 互联网架构阶段 Redis

Redis

零、 目录

  • 高并发思路
  • 电商网站中缓存数据库的设计
  • 缓存介绍
  • 按照redis
  • redis常用命令
  • redis其他数据结构
  • 数据分布式存储
  • Jedis客户端
  • 哈希一致性
  • 补充

一、 高并发思路

  1. 技术: tomcat集群+ nginx
    1. 理论上引入20台nginx — 100万/s的并发量
    2. 问题: 当100万个用户同时增删该查时 , 就出现了系统瓶颈
    3. 系统瓶颈: 后台程序能支持100万/秒的访问 , 但是数据库扛不住100万/s的访问
  2. redis
    1. 分布式 、 nosql 、 可以持久化+内存 、 内存 、 数据库
    2. 分布式: 数据库被划分
    3. nosql(not only sequence query language): 不仅仅支持关系型 、 结构化的数据库 , 而且支持非关系型 、 非结构化数据
    4. 可持久化+内存: 启动恢复机制(redis启动之后立即恢复之前的数据)
    5. 数据库: 数据存储
    6. 作用: 可以分布式的存储海量的数据 , 放到内存中 , 可以做缓存数据库

二、 电商网站中缓存数据库如何设计

  1. 缓存可以如何添加
    1. 数据库缓存:
      1. 执行的过程包括sql和组织查询结果 , 根据sql可以创建缓存 , 存储已经查过的resultSet , 节省了资源调度重组resultSet
    2. 持久层缓存:
      1. 减少数据库获取的结果转化为对象的过程 , 缓存直接调用保存的对象结果
    3. 业务层缓存:减少调用层次
    4. 控制层缓存: 减少调用层次
  2. 问题: 可以不可以过多的使用缓存?
    1. 缓存是占用内存空间的 ,过多的缓存插入 , 容易造成数据的冗余 , 在内存不够时 , 清空逻辑会交叉导致数据库失效
  3. 结论: 缓存引入的最终目的:
    1. 减少数据库的访问压力
    2. 减少网路传输
    3. 减少封装层次

三、缓存介绍

  1. 主流的缓存结构:
    1. ehcache(很多数据库底层缓存用的就是ehcache)并发量差
    2. memoryCache , 10年前 , 并发量高(100万左右/秒) , 缺点: 不落地(数据不能持久化 , 在宕机后不能立即恢复丢失的数据 , 严重的情况下容易造成缓存击穿 , 这也是被redis取代的主要原因)
    3. redis: 持久化 , 可以在宕机恢复后迅速的解决数据丢失的问题
  2. 问题: 如果在缓存服务器宕机后 , 无法进行数据恢复/没有解决数据丢失的问题 , 会导致“雪崩”(也叫缓存击穿)
    1. 雪崩(缓存击穿): 海量用户访问请求涌入 , 一旦缓存失效(宕机后缓存数据丢失) , 所有访问涌入数据库 , 数据库无法承受海量的数据的查询 , 导致数据库服务器宕机 , 这时重启数据库 , 但是请求没有消失甚至用户在不断的刷新(请求瞬间翻倍) , 就发生了数据库在重启->宕机->重启中循环 , 导致整个系统崩溃。
    1. 解决雪崩问题:
      1. 缓存永不宕机: 启动集群 , 永远让集群的一部分起作用 , 剩余的一部分做备用
      2. 缓存技术必须要支持恢复数据 , 持久化 。

四、 安装redis

  1. 登录linux , 并创建管理目录
  2. 下载安装包并解压

    下载命令
        wget "http://bj-yzjd.cn-bj.ufileos.com/redis-3.2.11.tar.gz"
    解压
        tar -xvf redis-3.2.11.tar.gz
    
  3. 进入解压之后的文件 , 执行安装

    make && make install
    
  4. 启动redis

    redis-server
    

    则启动成功

  5. 使用redis

    1. redis启动成功之后该linux主机立即成为一台redis服务器 , 此时使用redis时需要再打开一个该虚拟机的连接之后启动redis客户端
    2. 使用redis需要启动redis客户端

      redis-cli
      
    3. 如果想在同一个连接中启动服务和客户端 , 则启动redis时可以使redis服务器在后台运行

      redis-server &
      
  6. 停止redis服务

    1. 在占用控制台的服务连接中直接Ctrl+c即可停止服务
    2. 在客户端中

      shutdown
      
  7. 检查后台 运行的 redis

    ps -ef|grep redis
    
    1. redis-server 表示redis服务
    2. *表示所有IP都能够访问当前redis服务 , 如果列出一些列的IP地址 , 则除这些ip地址以外的访问都被拒绝。

五、 redis常用命令

  1. redis存储的数据实际上是map形式(key-value|{key , value}|list)的字符串
  2. keys : 获取当前存储空间中所有存在的key
  3. set [key] [value] : 设置key-value , key和value都是字符串
  4. get[key] :通过key获取对应的值
  5. select[整数值0~15] : redis默认存在0~15标号的数据库 分库 , 默认使用第一个库(0号库) , 这个功能是早期版本的冗余功能 , 现在的java代码不支持分库, 所以select 的功能逐渐不被使用
  6. exists [key] : 判断该key是否存在
    1. 与get的区别:
      1. 分析get: 在redis中一个字段允许存储的最大大小为512M
      2. 如果使用get查询 , 如果存在则会返回值 , 此时如果值过大会占用过多的资源 。
      3. 而exists只是判断key是否存在 , 如果存在会返回1 , 如果不存在则返回0.
  7. del [key] : 删除该key对应的键值对
  8. type [key] : 获取key对应值的类型 , 普通的数据类型都是string , 复杂的数据类型有map 和list
  9. help type/help[命令名称] 如:help set : 查看该命令的作用
  10. 实际问题可以在官网中查询对应的命令细节
  11. flushall : 将所有的数据(0~15号库) , flush到持久化文件中
  12. flushdb : flush当前分库的所有数据到持久化文件中 。
  13. incr [key] : 自增 , Integer类型的数据自增(redis中存储是都是String类型 , 在需要自增时, 会先试图转换成Integer在再增) , 如果转换不成功 , 则会报错, incrby [key] index 自增指定的步数
  14. decr [key]: 自减 decr [key] index 自减指定的步数
  15. append [key] [appendValue]: 在value后追加数据
  16. mget [k1] [k2] … : 获取一批key对应的值
  17. mset [k1] [v1] [k2] [v2] … : 设置一批数据 常用的编程语言的API一般不支持这个命令 ,因为使用这中群体操作k-v的命令后不支持数据分片(使用key取hash值取余后 散列存储)和集群计算 ; 这是一个早期的冗余功能
  18. expire [key] 时间数字(单位:秒) :设置当前key对应的value的过期时间
    1. ttl [key] : 查看当前key-value的存活时间
      1. -2 代表过期
      2. -1 代表永久
    2. 可以使用数据中的过期时间来做倒计时 , 或者秒杀 , 但是这个倒计时是秒级别的 。
  19. pexpire [key] 时间(单位毫秒) :做精确时间的秒杀

六、 redis其他数据结构

  1. Hash结构:
    1. 本身是key-value 的形式 , 但是这里的key也是key-value的形式
    2. hset [key] [field] [value] : 赋值
    3. hget [key] [field] : 取值
    4. hmset [key] [field] [value] [field1] [value1] :批量赋值
    5. hmget [key] [field] [field1]: 批量取值
    6. hexists : 查看属性是否存在 , 存在返回1 , 不存在返回0
    7. hdel [key] [field] :删除字段
    8. hkeys [key]:只获取字段名
    9. hvals [key] : 只获取字段值
    10. hlen [key]:获取字段数量
  2. list结构
    1. key-value(双向链表 , 左->上 , 右→下)
    2. lrange [listkey] start end :查看list
    3. lpush [key] value :向对应的list的头部添加信息 , 如果没有改;list则创键后添加
    4. rpush [key] [v1] : 向对应的list的尾部添加字符串元素
    5. linsert : 向对应的list的特定位置之前或之后添加字符串元素
    6. lset:设置list中指定下标的元素值
    7. lrem : 从key对应的list中删除count个value相同的元素
      1. count>0按从头到尾的顺序删除
      2. count<0 时按照从尾到头的顺序删除
      3. count=0 时 删除所有与value相同的元素
    8. ltrim : 保留指定key的值得范围内的数据
    9. lpop : 从list头部删除元素, 并返回删除的元素
    10. rpop : 从list尾部删除元素并返回删除的元素
    11. rpoplpush [list1] [list2]:从第一个尾部删除一个数据并将删除的数据添加到第二个list头部 , 整个操作是原子级的如果第一个list不存在或为空 则执行结果为nil , 如果第二个list不存在则创建
    12. lindex : 返回名称为key的list中index位置的元素
    13. llen : 返回list的长度

七、 数据分布式存储

  1. 要完成数据的分片存储 , 需要至少多个redis实例
  2. 启动多个redis时 , 每一个redis会占用一个端口 , 如果端口冲突 , 则会发生启动失败 , 所以要更改redis的默认配置文件
  3. 修改配置文件
    1. 进入到redis根目录下的redis.conf文件修改
    2. 直接输入:set number 使左侧的行号显示
    3. 第61行 把bind注释掉
    4. 第80行 保护模式关闭
    5. 第84行修改默认端口 , 避免和其他redis冲突 , redis默认是6379
    6. 第105行当客户端空闲时间1小时就自动断开连接 , 0秒表示不启用超时设置
    7. 第128行daemonize设置成yes让redis启动时由守护进程管理(也就是在后台执行)
    8. 第150行 , 不同的redis设置不同的pid文件(和端口同名)
    9. 设置日志级别 , 使用默认就行
    10. 设置flush动作规则 , 默认900秒以内 至少有1条数据改动 则执行flush , 在300秒以内至少有10条数据变动则执行flush , 在60秒以内至少有10000条数据有变动则执行flush操作 。 默认即可
    11. 修改完之后保存并退出
    12. 复制整个redis文件夹 为 r2 , r3
    13. 并且修改r2 r3中配置文件的端口和pid dump文件的名字
    14. 进入到r1目录下 , 执行redis-server redis6379.conf
    15. 进入到r2目录下 , 执行redis-server redis6380.conf
    16. 进入到r3目录下 , 执行redis-server redis6381.conf
    17. 执行完之后检测三个redis实例是否启动成功 ps -ef|grep redis
    18. 此时如果需要开启redis客户端 字需要 执行 redis-cli -p 端口号

八、 Jedis客户端

  1. redis集群部署完成 , 就是执行数据的存储
  2. 数据来源: 代码 执行过程中产生
  3. 如何使用代码来做redis数据的缓存? Jedis客户端
  4. 在使用之前需要先导入Jedis的jar包

    2.6.0
    
    
    
        redis.clients
        jedis
        ${jedis.version}
    
    
  5. Jedis示例:

    /**
         * 测试单个结点(单个redis)连接
         * */
        @Test
        public void test_01() {
            //创建Jedis对象 , 并在构造方法中设置redis主机的ip和占用的端口
            Jedis jedis = new Jedis("106.75.48.3",6379);
            //使用Jedis进行简单的操作
            //存数据
            jedis.set("name", "tianjie");
            //取数据
            String name = jedis.get("name");
            System.out.println("name="+name);
        }
    /**
     * 模拟数据缓存执行逻辑 , 数据库的查询操作
     * */
    @Test
    public void test_02() {
    
        System.out.println("用户开始查询数据");
        //模拟客户端传来的参数
        String name  = "name";
        //创建一个Jedis客户端
        Jedis jedis = new Jedis("106.75.48.3" , 6379);
        //执行逻辑 
        //1. 先查询缓存中是否有数据 , 如果有则返回缓存中的数据
        //2. 如果缓存中没有数据则去数据库中查询数据 , 并且 把查询到的数据存入缓存中 , 供后续使用 , 返回数据库中查询到的信息
        String gname  = jedis.get("name");
        if(gname != null && !gname.equals("")) {
            //缓存中有数据
            System.out.println("从缓存中获取到的数据为:name = "+gname);
        }else {
            //缓存中没有数据
            //执行从数据库查询数据   略
            String dbname = "outman";//数据库查询到的数据
            //将查询到的数据存入缓存中 
            jedis.set("name", dbname);
            //返回从数据库中查到的数据
            System.out.println("从数据库获取到的数据为:name = "+dbname);
        }
        //第一次执行结果为从数据库中获取
        //第二次执行结果我从缓存中获取数据
    }
    
  6. 自定义分片算法 ,将数据分片存入多个redis实例

    /**
         * 自定义分片计算逻辑
         * */
        @Test
        public void test_03() {
            //模拟需要存储的数据
            String k1 = "四十二章经第一章";
            String v1 = "111111111111111111";
            String k2 = "四十二章经第二章";
            String v2 = "222222222222222222";
            String k3 = "四十二章经第三章";
            String v3 = "333333333333333333";
            List keyList = new ArrayList();
            keyList.add(k1);
            keyList.add(k2);
            keyList.add(k3);
            Map map = new HashMap();
            map.put(k1, v1);
            map.put(k2, v2);
            map.put(k3, v3);
    
            for(String key : keyList) {
                if("四十二章经第一章".equals(key)) {
                    //存入第一个redis结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6379);
                    jedis.set(key, map.get(key));
                }else if("四十二章经第二章".equals(key)) {
                    //存入第二个redis结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6380);
                    jedis.set(key, map.get(key));
                }else if("四十二章经第三章".equals(key)) {
                    //存入第三个redis结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6381);
                    jedis.set(key, map.get(key));
                }
            }
        }
    
  7. 使用hash取余法将数据分片存储

    /**
         * 哈希取余分片存储逻辑
         * */
        @Test
        public void test_04() {
            //模拟需要被存储的数据
            List keyList = new ArrayList();
            Map map = new HashMap();
            for(int i = 0 ; i<100 ; i++) {
                String key  = "key_"+i;
                String  value = "value_"+i;
                keyList.add(key);
                map.put(key, value);
            }
            //使用哈希取余法分片存储
            //定义结点(redis实例)数量
            int n = 3;
            for(String key : keyList) {
                //执行哈希取余 , 哈希结果可能为负数 , 此时需要与Integer的最大数进行与操作
                Integer num =( key.hashCode()&Integer.MAX_VALUE )%n; 
                if(num == 0) {
                    //存入第一个结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6379);
                    jedis.set(key, map.get(key));
                    jedis.close();
                }else if(num == 1) {
                    //存入第二个结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6380);
                    jedis.set(key, map.get(key));
                    jedis.close();
                }else if(num == 2) {
                    //存入第三个结点
                    Jedis jedis = new Jedis("106.75.48.3" , 6381);
                    jedis.set(key, map.get(key));
                    jedis.close();
                }
            }
    
            //执行结果  100个键值对几乎均匀的 分布存储在三台redis实例上
        }
    
  8. jedis分片 , 使用的hash一致性

    /**
         * Jedis分片 使用哈希一致型完成数据分片存储(Jedis默认的分片算法)
         * */
        @Test
        public void test_05() {
            //需要构造存储多个reids实例信息 的list
            List jedisList = new ArrayList();
            //创建结点信息
            JedisShardInfo info1 = new JedisShardInfo("106.75.48.3" , 6379);
            JedisShardInfo info2 = new JedisShardInfo("106.75.48.3" , 6380);
            JedisShardInfo info3 = new JedisShardInfo("106.75.48.3" , 6381);
            //list保存结点信息
            jedisList.add(info1);
            jedisList.add(info2);
            jedisList.add(info3);
            //构造一个Jedis分片对象 , 将list闯入构造方法中 , 狗后续分片
            ShardedJedis jedis = new ShardedJedis(jedisList);
            //模拟海量数据执行数据分片存储
            for( int i= 0 ; i<1000 ; i++) {
                jedis.set("key_"+i, "value_"  + i);
            }
    
            jedis.close();
            //数据通过哈希一致型算法分片存储在了多个reids实例中
            //单数每一个reids的实例的数据量并不是完全平均的 , 会有一定量的数据偏移
        }
    
  9. jedis池的使用

    /**
         * Jedis池
         * */
        @Test
        public void test_06() {
            //需要构造存储多个reids实例信息 的list
            List jedisList = new ArrayList();
            //创建结点信息
            JedisShardInfo info1 = new JedisShardInfo("106.75.48.3" , 6379);
            JedisShardInfo info2 = new JedisShardInfo("106.75.48.3" , 6380);
            JedisShardInfo info3 = new JedisShardInfo("106.75.48.3" , 6381);
            //list保存结点信息
            jedisList.add(info1);
            jedisList.add(info2);
            jedisList.add(info3);
            //对于连接来将 类似于JDBC连接处可以设置很多参数
            JedisPoolConfig config = new JedisPoolConfig();
            //设置最大连接数
            config.setMaxTotal(200);
            //创建Jeids连接池
            ShardedJedisPool pool = new ShardedJedisPool(config, jedisList);
            //使用连接处获取数据
            ShardedJedis jedis = pool.getResource();
            for(int i = 0 ; i<100 ; i++) {
                String value = jedis.get("key_"+i);
                System.out.println("获取到key_"+i+"的值为"+value);
            }
            //归还连接
            pool.returnResource(jedis);
        }
    

九、哈希一致性

  1. 哈希是一种散列算法
  2. 使用哈希取余算法进行分片存储的问题 :
    1. 会造成大规模的数据倾斜(散列必定倾斜) , 而哈希一致型一定程度的解决了数据倾斜
    2. 使用哈希取余算法进行分片 存储之后 , 如果redis实例有变动 , 则数据迁移量过大 。
  3. 哈希取余算法导致数据迁移量巨大
    1. 当redis集群数量进行增加减少的时候 , n变化导致数据命中的变化量非常大 , 所以需要进行数据迁移
    2. 而且redis结点越多 , 数据迁移量越大
  4. 哈希一致型

    1. jedis中引入另外一种hash散列算法 — hash一致性
    2. 是由1997年麻绳理工的学生发明 : 其原理是引入一个 2^32-1个结点的整数环
    3. 把节点使用ip+端口做哈希散列计算 , 得到43亿中的一个值 , 投射到环中
    4. 然后把所有的数据key进行hash散列计算 也投射到环上
    5. 其中node代表的是redis , 其余的是数据
    6. 环上的数据会顺时针寻找最近的结点后存储
    7. 这样在redis增加或减少时 , 数据量的迁移是较少的 , 而且reids结点越多 , 数据量迁移越少
    8. 解决数据偏移问题

      1. 单独的使用节点的ip+端口做映射,毕竟节点数量是有限的
      2. 有可能在映射时的各自分布位置并不平均,导致数据偏移量非常大

        解决数据的平衡性引入虚拟节点
        node1的ip是192.168.40.156
        node2的ip是192.168.40.157
        各自引入2个虚拟节点(虚拟节点的数量是非常大的)
        node1-1=hash(192.168.40.156#1)
        node1-2=hash(192.168.40.156#2)
        node2-1=hash(192.168.40.157#1)
        node2-2=hash(192.168.40.157#2)
        每一个虚拟节点在哈希环上也会接收顺时针寻找最近节点的key们
        通过增加节点数量(虚拟的),完成数据的映射平衡
        凡是投影到node1-1,node1-2的key,都会中真实存储在node1中
        所以虚拟节点越多平衡性越好
        

补充:

  1. 数据库缓存
  2. hash特性
  3. 自己查去吧 哈哈

你可能感兴趣的:(大数据)