Redis


Redis学习笔记

1、NoSQL数据库

NoSQL数据库:非关系型数据库,

特点:

  • 方便拓展(数据之间不存在关系)

  • 数据量大,高性能(读写操作性能强)

  • 数据类型多样(不需要事先设计数据库)


2、NoSQL数据库的四大分类

KV键值存储数据库

  • redis...

列存储数据库

  • HBase

文档型数据量

  • MongoDB...

图形数据量

  • Neo4J、InfoGrid

3、Redis概述

Redis是什么?

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。


4、Redis安装

Windows下安装
  • 1、下载地址:https://github.com/tporadowski/redis/releases
  • 2、启动服务:通过redis安装目录下的redis-server.exe启动
  • 3、Redis命令行程序:redis-cli.exe
Linux下安装

5、Redis基础知识

redis有十六个数据库。默认使用的是第0个。

常用命令
  • select index:切换数据库,index-第几个数据库
  • dbsize:查看当前数据库使用的大小
  • **keys **:查看当前数据库的所有key
  • flushdb:清空当前数据库
  • flushall:清空所有数据库

Redis是单线程的

为什么Redis是单线程还这么快?

Redis是将所有数据存在内存中的,所以使用单线程,除去多线程的cpu切换上下文,效率才是最高的。


6、Redis-Key操作

  • exists key名称:存在返回1,不存在返回0
  • move key名称 1 : 移除指定的键值对
  • expire key名称 秒数 : 设置指定key多久后过期
  • ttl key名称 :查看该key还有多久过期,返回-2则已经过期
  • type key名称 :查看该key的类型

7、五大数据类型

String(字符串)
  • set key value 设置值

  • get key 获取值

  • exists key 判断当前值是否存在,存在为返回1,不存在返回0

  • append key"字符串" 给当前key的值追加字符串,如果key不存在,则新建

  • strlen key 获取当前key值的长度

  • incr key 当前key的值自增1(针对于整型)

  • decr key 当前key的值自减1(针对于整型)

  • incrby key20 当前key的值自增20(针对于整型)

  • decrby key10 当前key的值自减10(针对于整型)

  • getrange key 0 3 获取当前key的值的第0位到第3位的字符串(截取字符串)

  • getrange key 0 -1 获取当前key的值的全部字符串

  • **setrange key1 ***** 替换指定位置的字符串

  • setex key1 10 "hello222" 设置key1的值10秒后过期

  • ttl key1 查询key1多少秒过期,返回-2则已经过期

  • setnx key value 如果key不存在则创建,如果存在则创建失败

  • mset k1 v1 k2 v2 k3 v3 批量设置值

  • mget k1 k2 k3 批量获取

  • msetnx k2 v2 k4 v4 不存在才创建,一个创建失败所有失败

  • set user:1001:name zhangsan 设置对象(key的设计,对象名:编号:属性名)

  • getset key value 先获取值返回,再设置新值

List
  • 所有的list命令都是用l开头,先插入的在右侧
  • lpush city chongqing,beijing:向list集合左侧添加一个或者多个值
  • lrange city 0 -1:遍历集合(倒序)
  • rpush city hunan:向city集合右侧侧添加一个或者多个值
  • lpop city:移除city集合左侧第一个值
  • rpop city:移除city集合右侧第一个值
  • lindex city 0:获取city集合中指定位置角标的元素
  • llen city:获取city集合的长度
  • lrem city 3 chongqing:移除city集合中的指定个数的value
  • ltrim city 0 1:截取city集合中的指定范围的值
  • rpoplpush city city2:将city集合中的最右侧的值移动到city2集合的最左侧
  • lset city 0 aaa:替换city集合中指定下标的值为新值,该下标位置不存在值,则报错
  • linsert city before aaa aaabefore:在集合指定元素前,插入指定元素
  • linsert city after aaa aaaafter:在集合元素指定元素后,插入指定元素
  • 总结
    • redis的List集合本质是一个链表。
    • 操作两侧元素效率高,操作中间元素效率低。
set
  • set集合中所有命令s开头,存入的值不重复
  • sadd myset 111 222:向myset集合中添加一个或多个值,逗号隔开
  • smembers myset:取出myset集合中的所有值
  • sismember myset 111:判断该值在myset集合中是否存在,存在返回1,不存在返回0
  • scard myset:返回myset集合中的元素个数
  • srem myset 444 555:移除myset集合中的一个或多个值,逗号隔开
  • srandmember myset 2:随机取出myset集合中的指定个数元素,默认取出一个
  • spop myset 2:随机删除myset集合中的指定个数元素,默认删除一个
  • smove myset myset2 111:将指定元素从myset集合移动到myset2集合中去
  • sdiff key1 key2:返回key1,key2集合的元素差集,key1中存在,但key2中不存在的元素
  • sinter key1 key2:返回key1,key2集合的元素交集,相同元素
  • sunion key1 key2:返回key1,key2集合的元素并集,所有元素,去重。
hash(哈希)
  • hash仍然是key-value的形式存储元素,但value变为存放map集合

  • hset person name zhangsan:设置值

  • hget person name:取出person中,name为key的value

  • hgetall person:取出person中的所有键值对,结果如下:

    1) "name"
    2) "zhangsan"
    3) "age"
    4) "18"
    5) "address"
    6) "chongqing"
    
  • hmset person name zhangshan age 18 address beijing:设置多个键值对的值

  • hmget person name age address:取出多个键的值

  • hdel person age:删除指定key的键值对

  • hlen person:获取person中有多少个键值对

  • hexists person name:判断person中指定key是否存在,存在返回1,不存在返回0

  • hkeys person:获取person中的所有key

  • hvals person:获取person中的所有value

  • hincrby person age 2:给person中指定key的值增加指定增量

  • hsetnx person sex 1:person中设置的key存在则设置失败,不存在则可以设置

zset

在set的基础上,中间为添加一个值,用于排序

  • zadd myzset 2 two 3 three 4 four:添加一个或者多个值,key和值中间为排序位
  • zrange myzset 0 -1:遍历所有元素,从小到大排序元素
  • ZRANGEBYSCORE myzset -inf +inf:从小到大排序元素
  • zrevrange myzset 0 -1:从大到小排序元素
  • ZRANGEBYSCORE myzset 2 3 withscores:查询排序字段在2到3之间的元素,并且带上排序位的值
  • zrem myzset zero:移除具体元素
  • zcard myzset:返回有序集合中的元素个数
  • zcount myzset 0 10:获得指定区间的成员数量

8、三大特殊数据类型

GEO

地理位置,通过经纬度确定位置。

  • geoadd:添加一个或多个地理位置坐标

    规则:两级无法添加,实际通过程序导入地理位置信息

    	 有效经度范围:-180到180度
    																																										
       有效纬度范围:-85.05112878至85.05112878度
    

    超过此范围,redis会返回错误信息。

    GEOADD key longitude latitude member [longitude latitude member ...]
    
    longitude:地理位置的经度
    latitude:地理位置的纬度
    member:地理位置名称
    
  • geopos:根据名称查询一个或多个地理位置信息

    GEOPOS key member [member ...]
    
  • geodist:获取两个地理位置的之间的距离

    GEODIST key member1 member2 [m|km|ft|mi]
    
    单位
    m:米(默认)
    km:千米
    ft:英尺
    mi:英里
    
  • georadius:获取指定经纬度坐标的指定范围内的地理位置集合

    GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]
    

    longitude latitude标识了地理位置的坐标,radius表示范围距离(多大半径距离),距离单位可以为m|km|ft|mi,还有一些可选参数:

    • WITHCOORD:传入WITHCOORD参数,则返回结果会带上匹配位置的经纬度。
    • WITHDIST:传入WITHDIST参数,则返回结果会带上匹配位置与给定地理位置的距离。
    • ASC|DESC:默认结果是未排序的,传入ASC为从近到远排序,传入DESC为从远到近排序。
    • WITHHASH:传入WITHHASH参数,则返回结果会带上匹配位置的hash值。
    • COUNT count:传入COUNT参数,可以返回指定数量的结果。
  • georadiusbymember:获取指定地理位置的指定范围内的地理位置集合

    GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]
    
  • geohash:返回一个或多个地理位置的hash值

Hyperloglog

用于做基数(不重复元素)统计的算法。所占用的内存很小。

  • PFADD key element [element ...]:添加指定元素到 HyperLogLog 集合中
  • PFCOUNT key [key ...]:返回给定 HyperLogLog 集合的基数估算值
  • PFMERGE destkey sourcekey [sourcekey ...]:将多个 HyperLogLog 合并为一个 HyperLogLog,
    • destkey:合并后的新HyperLogLog集合的名称
Bitmap

位图,按顺序储存0和1两种状态。

  • SETBIT key offset value:设置指定下标的状态(0或1)

    • 顺序必须按照0开始,每次设置+1的规则
  • GETBIT key offset:查看指定下标的状态值

  • bitcount key [start end]:统计key集合中,状态为1的数量,不指定开始和结束下标,默认统计全部

9、事务

  • redis事务的本质:是一组命令的集合!将redis命令按顺序执行
  • redis事务的特性:
    • 一次性
    • 顺序性
    • 排他性

redis的事务没有事务隔离的概念,redis单条命令保证原子性,但事务不保证原子性。

redis开启事务后,命令只进行入队操作,只有发起执行事务命令时,才会执行。

  • redis事务的使用:

    • 开启事务:multi

    • 命令入队:需要执行的redis命令集合

    • 执行事务:exec

      在开启事务后,可以使用 discard 命令放弃事务。即所有的命令失效,返回到开启事务前。

      127.0.0.1:6379> multi #开启事务
      OK
      127.0.0.1:6379> set k1 v1 #redis命令
      QUEUED
      127.0.0.1:6379> set k2 v2
      QUEUED
      127.0.0.1:6379> set k3 v3
      QUEUED
      127.0.0.1:6379> exec #执行事务
      1) OK	# 执行结果
      2) OK
      3) OK
      
      127.0.0.1:6379> multi #开启事务
      OK
      127.0.0.1:6379> set q1 v1
      QUEUED
      127.0.0.1:6379> set q2 v2
      QUEUED
      127.0.0.1:6379> discard #放弃事务
      OK
      
  • redis事务出现异常:

    • 编译型异常,即redis命令格式出错

      事务队列中的所有命令都不会执行。出现事务错误异常

    127.0.0.1:6379> multi 
    OK
    127.0.0.1:6379> set k1 #命令出错
    (error) ERR wrong number of arguments for 'set' command
    127.0.0.1:6379> set k1 k2
    QUEUED
    127.0.0.1:6379> exec #返回错误提示
    (error) EXECABORT Transaction discarded because of previous errors.
    
    • 运行时异常,redis命令在执行时,才会出现的错误

      事务队列中,出错的命令不会执行,正确的命令会执行

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set k1 "aaa" 
    QUEUED
    127.0.0.1:6379> incr k1 #错误语法:对字符串进行+1操作
    QUEUED
    127.0.0.1:6379> set k2 v2
    QUEUED
    127.0.0.1:6379> set k3 v3
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) (error) ERR value is not an integer or out of range #错误命令执行出错。其余执行成功
    3) OK
    4) OK
    
  • redis监视:

    redis中可以通过watch命令对key进行监视,如果在开启事务过程中,该key的值发生变化,则执行事务失败。

    127.0.0.1:6379> set money 100
    OK
    127.0.0.1:6379> watch money #监听money的值
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> decrby money 10
    QUEUED
    127.0.0.1:6379> set k1 v1
    QUEUED
    127.0.0.1:6379> exec
    (nil)  # 由于开启事务后,money的值发生变化,所以执行事务失败,返回null
    

10、Jedis

jedis是redis官方推荐的java连接工具,使用Java操作redis的中间件。

Jedis的使用
  • 导入依赖

    
    
        redis.clients
        jedis
        3.3.0
    
    
  • 创建对象

    //创建jedis对象,参数:地址+端口
    Jedis jedis = new Jedis("127.0.0.1",6379);
    //jedis类的方法和redis命令一致
    //测试连接
    System.out.println(jedis.ping());
    jedis.set("name","zhangsan");
    String name = jedis.get("name");
    System.out.println(name);
    
Jedis整合redis事务
//创建jedis对象
Jedis jedis = new Jedis("127.0.0.1", 6379);
//开启事务,multi:事务对象
Transaction multi = jedis.multi();
//执行命令操作
try {
    //执行命令
    multi.set("address1","beijing");
    multi.set("address2","chongqing");
    //未出现异常,执行事务
    multi.exec();
} catch (Exception e) {
    //出现异常,放弃事务
    multi.discard();
    e.printStackTrace();
}finally {
    System.out.println(jedis.get("address1"));
    System.out.println(jedis.get("address2"));
    //关闭连接
    jedis.close();
}

11、SpringBoot整合Redis

SpringBoot在2.X版本后,连接redis的底层工具由jedis替换为lettuce

对比:

? jedis:采用直连,多线程操作不安全,如果想实现线程安全,需要使用jedis pool连接池

? lettuce:底层采用netty,实例可以在多个线程中共享,线程安全

  • 1、引入坐标依赖

    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
  • 2、配置连接

    # 配置redis
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    
  • 3、通过RedisTemplate操作redis

    @Autowired
    private RedisTemplate redisTemplate;
    
    @Test
    void contextLoads() {
    
        redisTemplate.opsForValue().set("address1","chongqing");
        System.out.println(redisTemplate.opsForValue().get("address1"));
    }
    
RedisTemplate的使用

https://blog.csdn.net/ruby_one/article/details/79141940

RedisTemplate 对五种数据结构的操作
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

12、自定义RedisTemplate

redisTemplate无法存入未序列化的对象

解决方案:

  • 实体类实现序列化接口

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
    
        private String username;
        private Integer age;
    }
    
  • 将Java对象转换为json字符串存入

    User user = new User("李四", 33);
    //对象转为json字符串
    String userJson = new ObjectMapper().writeValueAsString(user);
    
    redisTemplate.opsForValue().set("user",userJson);
    
自定义RedisTemplate

由于redis自动配置类中存在@ConditionalOnMissingBean(name = "redisTemplate")注解,所以当自定义redisTemplate时,自动配置类会失效。

/**
 * 自定义redisTemplate注解
 * @param factory
 * @return
 */
@Bean
@SuppressWarnings("all")
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    //json序列化配置
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

    //String的序列化
    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);

    return template;
}
RedisTemplate的工具类
@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection) CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

13、windows环境下,redis配置文件的几点设置

  • windows下redis服务默认加载redis.windows-service.conf配置文件,修改为加载redis.windows.conf文件

    • 1、删除windows服务下的redis服务

      • a、通过redis服务-->属性-->找到服务名称
      • b、管理员打开cmd窗口
      • c、执行命令:sc delete 服务名称
    • 2、注册redis服务

      使用cmd进入redis目录运行 redis-server --service-install redis.windows.conf --service-name redis
      
    • 在windows服务中,将redis服务设为自动

  • 在windows服务中,手动启动redis服务,即可后台运行redis服务

  • 通过redis-server.exe启动redis服务,会找不到配置文件

     Warning: no config file specified, using the default config. In order to specify a config file use c:\java\redis\redis-server.exe /path/to/redis.conf
    
    • 通过配置文件启动服务:redis-server.exe redis.windows.conf

14、redis.conf配置文件属性

units 单位

配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit.单位大小写不敏感 1GB 1Gb 1gB 是一样的。

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
INCLUDES 包含

导入其他redis配置文件

# include .\path\to\local.conf
# include c:\path\to\other.conf
NETWORK 网络
bind 127.0.0.1	#绑定访问redis数据库的ip地址
protected-mode yes	#是否开启保护模式
port 6379	#端口设置
  • tcp-backlog 511

    设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 已经完成三次握手队列。在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconntcp_max_syn_backlog两个值来达到想要的效果。

  • timeout 0

    设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接。0为关闭

  • Tcp-keepalive 0

    单位为秒,指定 TCP 连接是否为长连接,"侦探"信号有 server 端维护。默认为 0,表示禁用,则不会进行Keepalive检测

    GENERAL 通用
    daemonize no	#是否以守护进程开启,默认为no
    
  • loglevel notice :日志等级,四种等级

    • debug:该等级,会产生大量日志信息,一般用于测试和开发阶段。
    • verbose:记录较多的日志信息,与debug等级相似。
    • notice:(默认等级),记录少量的日志信息,适用于生产环境。
    • warning:打印非常重要或关键的日志信息。
  • logfile "":日志的输出文件位置地址。为空时直接日志直接打印在控制台。

  • databases 16:redis默认16个数据库。

SNAPSHOTTING 快照

快照:数据持久化到本地硬盘的方式之一

# 保存数据快照的频率,即将数据持久化到 dump.rdb 文件中的频度。
save 900 1	# 900秒内有一个key改变过就备份
save 300 10	# 300秒内有10个key改变过就备份
save 60 10000	# 60秒内有10000个key改变就触发备份
#持久化出错时,redis是否继续工作。
stop-writes-on-bgsave-error yes	

#是否压缩rdb文件,需消耗cpu资源。
rdbcompression yes	

# 在存储快照后,还可以让redis使用CRC64算法来进行数据检验
# 这样做会增加10%的性能消耗,
# 如果想获得最大的性能提升,则可以关闭此功能
rdbchecksum yes 

# 保存时的文件名称,断电重启时读取的文件名称
dbfilename dump.rdb

# rdb文件保存的目录
dir ./

SECURITY 安全
# 获取登录密码
config get requirepass
127.0.0.1:8686> config get requirepass
1) "requirepass"
2) "51310400"
# 查询启动时所在的目录
config get dir
127.0.0.1:8686> config get dir
1) "dir"
2) "/alidata/redis-5.0.3/db"

# 设置redis密码
config set requirepass 123456

# 登录redis
[root@izm5e2q95pbpe1hh0kkwoiz /]# redis-cli -p 8686
127.0.0.1:8686> ping
(error) NOAUTH Authentication required.
127.0.0.1:8686> auth 51310400
OK
127.0.0.1:8686> ping
PONG
LIMITS 限制
  • maxclients 10000:能连上redis数据库的最大客户端数量

  • maxmemory :redis 配置的最大的内存容量,达到最大内存时启动清除策略。

  • maxmemory-policy noeviction:内存容量达到上限的处理策略

    • (1)volatile-lru:使用LRU算法移除key,只对设置了过期时间的键
      (2)allkeys-lru:使用LRU算法移除key
      (3)volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
      (4)allkeys-random:移除随机的key
      (5)volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
      (6)noeviction:不进行移除。针对写操作,只是返回错误信息
  • maxmemory-samples 5

    设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,
    redis默认会检查这么多个key并选择其中LRU的那个

APPEND ONLY MODE

aof配置

  • appendonly no:默认不开启aof持久化模式,默认使用rdb持久化模式。
  • appendfilename "appendonly.aof":aof持久化文件名
  • appendfsync everysec:执行同步的模式
    • always:每次修改都会同步。消耗内存
    • everysec:每秒执行同步,有可能会丢失这一秒的数据。
    • no:不执行同步,操作系统自动同步,速度最快。

15、Redis持久化

Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能

RDB持久化

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发自动触发

RDB 持久化功能所生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态

  • 手动触发rdb持久化

    • save命令:阻塞当前Redis服务器直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用
    • bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
  • 自动触发rdb持久化

    • 配置根据key发生改变的频率,触发rdb持久化

        # 保存数据快照的频率,即将数据持久化到 dump.rdb 文件中的频度。
        save 900 1	# 900秒内有一个key改变过就备份
        save 300 10	# 300秒内有10个key改变过就备份
        save 60 10000	# 60秒内有10000个key改变就触发备份
      
    • 执行shutdownflushall命令时,且没有开启AOF持久化功能则自动执行bgsave。

  • 恢复rdb持久化的数据

    将备份文件(dump.rdb)放到redis启动目录下,redis启动时,会自动加载备份的数据。

  • 优点

    • Redis加载RDB恢复数据远远快于AOF的方式(适合大规模的数据恢复)。
    • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复(对数据完整性和一致性要求不高
  • 缺点

    • 数据丢失:在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。

    • 版本不兼容:RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

    • 影响性能:fork子进程的时候,会消耗内存。

AOF持久化

在redis操作时,每当有修改数据库的命令时,会将命令追加写入到aof文件的末尾,在恢复数据库时,会将aof文件中的修改命令依次执行,还原数据库数据。

配置在配置文件中的APPEND ONLY MODE 模块中,aof持久化默认是不开启的,如果需要开启,需将appendonly属性改为yes

appendonly yes

appendfsync everysec #执行同步的模式
#always:每次修改都会同步。消耗内存
#everysec:每秒执行同步,有可能会丢失这一秒的数据。
#no:不执行同步,操作系统自动同步,速度最快。
  • redis在启动时,会通过appendonly.aof文件恢复数据。

    :windows环境下,redis-server.exe默认不会通过redis.windows.conf配置文件启动服务,所以aof配置和恢复数据将失效。在启动服务时,通过配置文件启动服务:redis-server.exe redis.windows.conf

  • aof文件遭到破坏时,redis服务会启动失败,出现以下错误:

     Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix 
    

    通过redis-check-aof工具,修复appendonly.aof文件,会aof文件中问题命令以后的所有内容,只保留没问题的命令:

    C:\Java\Redis>redis-check-aof.exe --fix appendonly.aof	#windows下的命令
    0x66: Expected \r\n, got: 6164
    AOF analyzed: size=196, ok_up_to=81, diff=115
    This will shrink the AOF from 196 bytes, with 115 bytes, to 81 bytes
    Continue? [y/N]: y
    
  • 优点:

    • 模式为每次修改都同步时,可以保证数据的完整性。
    • 模式为每一秒同步一次修改,可能丢失最后一秒数据。
    • 从不同步,效率最高
  • 缺点:

    • aof文件远远大于rdb文件,文件恢复速度也非常慢。
扩展:

1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

5、性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。
  • 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
  • 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。
如何选择使用哪种持久化方式?

? 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

? 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

? 有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进 行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

16、Redis发布订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

  • [PSUBSCRIBE pattern [pattern ...]] :订阅一个或多个符合给定模式的频道。
  • [PUBSUB subcommand [argument [argument ...]]]:查看订阅与发布系统状态。
  • [PUBLISH channel message]:将信息发送到指定的频道。
  • [PUNSUBSCRIBE [pattern [pattern ...]]]:退订所有给定模式的频道。
  • [SUBSCRIBE channel [channel ...]]:订阅给定的一个或多个频道的信息。
  • [UNSUBSCRIBE [channel [channel ...]]]:指退订给定的频道。
演示
  • 发送端

    127.0.0.1:6379> PUBLISH pd1 "hello redis" #向pd1频道发送消息,返回值为:成功接收的订阅者个数。
    (integer) 1
    127.0.0.1:6379> PUBLISH pd1 "22222"
    (integer) 2
    
  • 接收端

    127.0.0.1:6379> PSUBSCRIBE pd1 #订阅pd1频道
    Reading messages... (press Ctrl-C to quit)
    1) "psubscribe"
    2) "pd1"
    3) (integer) 1  #订阅成功,等待接收消息
    1) "pmessage"  #类型为消息
    2) "pd1" # 频道名称
    3) "pd1"
    4) "22222" # 消息内容
    

    原理

    Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。

    Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。

    每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

    img

    客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

    缺点

    1. 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
    2. 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。

    应用

    1. 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
    2. 多人在线聊天室。

    稍微复杂的场景,我们就会使用消息中间件MQ处理。

17、Redis主从复制

概述:
  • 概念

    主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。

    默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。

  • 作用

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
  2. 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
  3. 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
  4. 高可用基石:主从复制还是哨兵和集群能够实施的基础。
  • 为什么使用集群

    一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

    1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;

    2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

    电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。

    主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从!

    只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!

  • 总结

  1. 单台服务器难以负载大量的请求
  2. 单台服务器故障率高,系统崩坏概率大
  3. 单台服务器内存容量有限。
环境查看

只配置从库,不用配置主库!

127.0.0.1:6379> info replication
# Replication
role:master # 角色
connected_slaves:0 # redis从机的数量
master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
windows下开启多个redis服务
  • 1、复制redis配置文件。

  • 2、修改新复制的备份文件的信息,防止冲突。内容包括:

    port 6380 #端口号
    
    logfile "6380.log" #日志文件名称
    
    dbfilename dump6380.rdb #持久化文件名
    
  • 3、根据这些配置文件安装redis服务,进入redis安装目录,执行命令启动服务。

    redis-server.exe --service-install redis.6380.conf --service-name redis6380
    redis-server.exe --service-install redis.6381.conf --service-name redis6381
    redis-server.exe --service-install redis.6382.conf --service-name redis6382
    ......
    
  • 4、在windows服务中,手动开启服务

  • 5、通过命令分别连上redis服务

    redis-cli.exe -h 127.0.0.1 -p 6380
    redis-cli.exe -h 127.0.0.1 -p 6381
    redis-cli.exe -h 127.0.0.1 -p 6382
    
一主二从

默认情况下,每台redis服务器都是主机(master),只需要配置从机。

临时配置从机(通过命令行配置)
  • 在从机中配置谁为主机,通过配置跟随谁。

    127.0.0.1:6380> SLAVEOF 127.0.0.1 6379  # SLAVEOF+主机ip+主机端口
    OK
    127.0.0.1:6380> info replication
    # Replication
    role:slave	#该服务器,角色已变为从机
    master_host:127.0.0.1 #主机的ip
    master_port:6379 #主机的端口
    master_link_status:up
    master_last_io_seconds_ago:9
    master_sync_in_progress:0
    slave_repl_offset:14
    slave_priority:100
    slave_read_only:1
    connected_slaves:0
    master_replid:92fece5c1b55c582589fef7676bf417b38e233c5
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:14
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:14
    

    从机配置完成后,在主机中可以查看到从机的信息。

    127.0.0.1:6379> info replication
    # Replication
    role:master
    connected_slaves:1 #从机数量
    slave0:ip=127.0.0.1,port=6380,state=online,offset=210,lag=0  #从机信息
    master_replid:92fece5c1b55c582589fef7676bf417b38e233c5
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:210
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:210
    
永久配置从机(修改配置文件)
slaveof  
细节
  • 主机可以读写,从机只能读不能写,主机中的数据,会同步到从机中。

    主机中写入数据

    127.0.0.1:6379> set name zhangsan
    OK
    

    从机中读取数据

    127.0.0.1:6380> get name #从机中读取主机中写入的数据
    "zhangsan"
    127.0.0.1:6380> set address beijing #从机中写入数据会报错
    (error) READONLY You can't write against a read only replica.
    
  • 主机断开连接(宕机)

    • 默认从机还是连接到主机上的,但确实主机的写操作。
    • 如果断开的主机重新连接,从机也能从主机上同步数据。
  • 从机断开连接(宕机)

    • 如果是命令行配置的从机,该从机变为主机。
    • 只有当重新将该服务器,配置上主机,才能从主机获取数据。
复制原理
  • Slave 启动成功连接到 master 后会发送一个sync同步命令

  • Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

  • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

  • 增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步,但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到!

从机变为主机
  • 当主机断开连接时,从机可以使用命令:SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!

18、哨兵模式

概述
  • 主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

  • 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

哨兵的作用
  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
多哨兵模式(哨兵集群)
  • 然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式

  • 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

开启哨兵模式
  • 1、编写哨兵模式配置文件(命名必须为:sentinel.conf)

    ? 最少配置如下:

    # sentinel monitor 被监控的名称 主机ip 主机端口 1
    sentinel monitor myredis 127.0.0.1 6379 1
    
  • 2、启动哨兵模式

    • windows环境下

      redis-server.exe sentinel.conf --sentinel
      
    • linux环境下

      redis-sentinel myconf/sentinel.conf
      
  • 3、哨兵模式启动成功

    C:\Java\Redis>redis-server.exe sentinel.conf --sentinel
    [14064] 18 May 22:59:57.768 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    [14064] 18 May 22:59:57.768 # Redis version=5.0.10, bits=64, commit=1c047b68, modified=0, pid=14064, just started
    [14064] 18 May 22:59:57.768 # Configuration loaded
                    _._
               _.-``__ ''-._
          _.-``    `.  `_.  ''-._           Redis 5.0.10 (1c047b68/0) 64 bit
      .-`` .-```.  ```\/    _.,_ ''-._
     (    '      ,       .-`  | `,    )     Running in sentinel mode
     |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
     |    `-._   `._    /     _.-'    |     PID: 14064
      `-._    `-._  `-./  _.-'    _.-'
     |`-._`-._    `-.__.-'    _.-'_.-'|
     |    `-._`-._        _.-'_.-'    |           http://redis.io
      `-._    `-._`-.__.-'_.-'    _.-'
     |`-._`-._    `-.__.-'    _.-'_.-'|
     |    `-._`-._        _.-'_.-'    |
      `-._    `-._`-.__.-'_.-'    _.-'
          `-._    `-.__.-'    _.-'
              `-._        _.-'
                  `-.__.-'
    
    [14064] 18 May 22:59:57.772 # Sentinel ID is 1dacfa6cc5335ee44b10f6f205b82371fa7188f8
    [14064] 18 May 22:59:57.773 # +monitor master redis 127.0.0.1 6379 quorum 1 监听主机
    [14064] 18 May 22:59:57.774 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis 127.0.0.1 6379
    [14064] 18 May 22:59:57.775 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.249 # +sdown master redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.249 # +odown master redis 127.0.0.1 6379 #quorum 1/1 一个哨兵认为主机断开
    [14064] 18 May 23:00:51.250 # +new-epoch 1
    [14064] 18 May 23:00:51.250 # +try-failover master redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.251 # +vote-for-leader 1dacfa6cc5335ee44b10f6f205b82371fa7188f8 1
    [14064] 18 May 23:00:51.251 # +elected-leader master redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.251 # +failover-state-select-slave master redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.358 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.358 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.470 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.902 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.902 # +failover-state-reconf-slaves master redis 127.0.0.1 6379
    [14064] 18 May 23:00:51.965 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:52.923 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:53.938 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ redis 127.0.0.1 6379
    [14064] 18 May 23:00:54.001 # +failover-end master redis 127.0.0.1 6379   主机断开连接
    [14064] 18 May 23:00:54.001 # +switch-master redis 127.0.0.1 6379 127.0.0.1 6381 选举6381作为新主机
    [14064] 18 May 23:00:54.002 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis 127.0.0.1 6381
    [14064] 18 May 23:00:54.002 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis 127.0.0.1 6381
    [14064] 18 May 23:01:24.089 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis 127.0.0.1 6381
    [14064] 18 May 23:02:40.044 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis 127.0.0.1 6381
    [14064] 18 May 23:02:50.023 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis 127.0.0.1 6381 # 原主机6379重新连接,但只能作为6381的从机
    

19、Redis缓存穿透和雪崩

缓存穿透(即查询不到)
概念
  • 在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
  • 布隆过滤器

    对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

    img

  • 缓存空对象

    一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。

    img

    这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间

    即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(即量太大,缓存过期)
概念
  • 相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。

  • 比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。

解决方案
  • 1.设置热点数据永不过期

    这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

  • 2.加互斥锁(分布式锁)

    在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

缓存雪崩
概念
  • 大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

  • 缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

  • 产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

    img

  • 其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

redis高可用

  • 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群

限流降级

  • 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

  • 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。