Redis

1、什么是Redis

  • NoSql数据库
  • 分布式缓存中间件
  • key-value存储
  • 提供海量数据存储访问
  • 数据存储在内存里,读取更快

2、缓存方案对比

缓存方案

优点

缺点

Ehcache

  1. 基于Java开发
  2. 基于JVM缓存
  3. 简单、轻巧、方便
  1. 集群不支持(缓存不共享)
  2. 分布式不支持

Memcache

  1. 简单的key-value存储(单一String类型)
  2. 内存使用率比较高
  3. 多核处理,多线程
  1. 无法容灾
  2. 无法持久化

Redis

  1. 丰富的数据结构
  2. 持久化
  3. 主从同步、故障转移
  4. 内存数据库
  1. 单线程(6.0加入多线程)
  2. 单核

3、Redis的数据类型

3.1 string

string:最简单的字符串类型键值对缓存。

key相关

keys * : 查看所有的key(不建议在生产环境上使用,有性能影响)

type key: key的类型

string类型

  • get/set/del:查询/设置/删除
  • set key value: 设置key(已存在,则覆盖)
  • setnx key value: 设置key(已存在,不覆盖)
  • set key value ex time: 设置带过期时间的数据
  • expire key: 设置过期时间
  • ttl: 查看过期时间, -1-永不过期, -2已过期
  • append key: 追加字符串
  • strlen key:字符串长度
  • incr key: 数字值累加一
  • decr key:数字值累减一
  • incrby key num: 累加给定数值
  • decrby key num: 累减给定数值
  • getrange key start end: 截取数据, end=-1 代表最后
  • setrange key start newdata: 从start位置开始替换数据
  • mset: 连续设值
  • mget: 连续取值
  • msetnx: 连续设值不覆盖
3.2 list

list:列表,[a,b,c,d,…]

  • lpush userList 1 2 3 4 5:构建一个list,从左边开始存入数据
  • rpush userList 1 2 3 4 5:构建一个list,从右边开始存入数据
  • lrange list start end:获得数据
  • lpop:从左侧开始拿出一个数据
  • rpop:从右侧开始拿出一个数据
  • llen list:list长度
  • lindex list index:获取list下标的值
  • lset list index value:把某个下标的值替换
  • linsert list before/after value:插入一个新的值
  • lrem list num value:删除几个相同数据
  • ltrim list start end:截取值,替换原来的list
3.3 hash

hash:类似map,存储结构化数据结构,比如存储一个对象(不能有嵌套对象)

  • hset key property value:
    -> hset user name xybh
    -> 创建一个user对象,这个对象中包含name属性,name值为xybh
  • hget user name:获得用户对象中name的值
  • hmset:设置对象中的多个键值对
    -> hset user age 18 phone 139123123
  • hmsetnx:设置对象中的多个键值对,存在则不添加
    -> hset user age 18 phone 139123123
  • hmget:获得对象中的多个属性
    -> hmget user age phone
  • hgetall user:获得整个对象的内容
  • hincrby user age 2:累加属性
  • hincrbyfloat user age 2.2:累加属性
  • hlen user:有多少个属性
  • hexists user age:判断属性是否存在
  • hkeys user:获得所有属性
  • hvals user:获得所有值
  • hdel user:删除对象
3.4 set

set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

  • sadd: 增加新元素
  • sismember: 判断元素是否在set中
  • sinter: 查看多个set的交集
  • spop: 随机删除一个或多个元素并返回
  • srandmember:随机获取一个元素
  • scard: 获取set的成员数
  • key: 返回集合中的所有成员
3.5 zset

sorted set:排序的set,可以去重可以排序,比如可以根据用户积分做排名,积分作为set的一个数值,根据数值可以做排序。

  • zadd zset 10 value1 20 value2 30 value3:设置member和对应的分数
  • zrange zset 0 -1:查看所有zset中的内容
  • zrange zset 0 -1 withscores:带有分数
  • zrank zset value:获得对应的下标
  • zscore zset value:获得对应的分数
  • zcard zset:统计个数
  • zcount zset 分数1 分数2:统计个数
  • zrangebyscore zset 分数1 分数2:查询分数之间的member(包含分数1 分数2)
  • zrangebyscore zset (分数1 (分数2:查询分数之间的member(不包含分数1 和 分数2)
  • zrangebyscore zset 分数1 分数2 limit start end:查询分数之间的member(包含分数1 分数2),获得的结果集再次根据下标区间做查询
  • zrem zset value:删除member

redis sorted sets里面当items内容大于64的时候同时使用了hash和skiplist两种设计实现。这也会为了排序和查找性能做的优化。所以如上可知:

添加和删除都需要修改skiplist,所以复杂度为O(log(n))。

但是如果仅仅是查找元素的话可以直接使用hash,其复杂度为O(1)

其他的range操作复杂度一般为O(log(n))

当然如果是小于64的时候,因为是采用了ziplist的设计,其时间复杂度为O(n)

4、Redis线程模型

redission isHeldByCurrentThread 失败 redis is not empty_java

请求过程:

Redis-cli发送Readable/Writable事件,使用Socket与Redis-server通信,Redis-server使用多路复用器(同步非阻塞)将事件发送到文件事件分配器,文件事件分配器根据请求类型转发至连接应答处理器/命令请求处理器/命令应答处理器

redission isHeldByCurrentThread 失败 redis is not empty_分布式_02

5、发布与订阅

redission isHeldByCurrentThread 失败 redis is not empty_分布式_03

5.1 发布

publish channel messgae: 将消息发送到指定的频道。

5.2 订阅

psubscribe pattern 订阅一个获多个符合给定模式的频道

pubsub subcommand 查看订阅和发布系统状态

subscribe channel 订阅给定的一个或多个频道的信息

unsubscibe channel 退订给定的频道

6、Redis读写分离(主从架构)

6.1 主从架构
Redis主从架构: master节点做到一个分发命令的功能,主节点将数据复制给从库节点。(水平扩展,通过增加服务器来提高性能)

redission isHeldByCurrentThread 失败 redis is not empty_redis_04

6.2 主从原理
  1. 从Redis第一次连接主Redis,使用全量复制
  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
  1. 增量同步
    Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
    增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。(Master必须要开启持久化)

redission isHeldByCurrentThread 失败 redis is not empty_缓存_05

6.3 主从模式

一般采用一主二从,一主多从比较少,因为主从同步需要占用带宽,较多从节点可能会占用较多带宽。

redission isHeldByCurrentThread 失败 redis is not empty_redis_06

7、Redis缓存过期机制

  1. (主动)定期删除
  • 定时随机的检查过期的key,如果过期则清理删除。(每秒检查次数在redis.conf中的hz配置)
  1. (被动)惰性删除
  • 当客户端请求一个已经过期的key的时候,那么redis会检查这个可以是否过期,如果过期了,则删除,然后返回一个nil。

如果内存被Redis缓存占用满了怎么办?

当内存占用满了后,redis提供了一套缓存淘汰机制:MEMORY MANAGEMENT

  • noevivcation:旧缓存永不过期,新缓存设置不了,直接返回错误
  • allkeys-lru:清除所有键中最少使用的旧缓存,然后保存新缓存(推荐使用)
  • allkeys-random:在所有缓存中随机删除(不推荐)
  • volatile-lru:在设置了过期时间的缓存中,清除最少用的旧缓存,然后保存新的缓存
  • volatile-random:在设置了过期时间的缓存中,随机删除缓存
  • volatile-ttl:在设置了过期时间的缓存中,删除即将过期的缓存

8、Redis哨兵模式

redission isHeldByCurrentThread 失败 redis is not empty_redis_07

  1. 配置sentinel.conf(默认端口 26379)
  2. sentinel monitor master_name ip_address port quorum(quorum 哨兵检测数量)
  3. sentinel auth-pass master_name password

9、Redis异常

9.1 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,异常请求直接拦截
  2. 在缓存和数据库都取不到数据的情况下,可以增加key-null的短期缓存(如30秒,5分钟)
  3. 设置布隆过滤器(存在误判率,返回无数据必为无数据,有数据不一定有数据)
9.2 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 设置不同的过期时间
  2. 设置热点信息永不过期
  3. 多缓存结合
  4. 使用分布式Redis,热点信息存储在不同的Redis中
9.3 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:

  1. 设置热点信息永不过期
  2. 加互斥锁

工具类

RedisOperator.java

public class RedisOperator {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 键 操作

    /**
     * 实现 TTL key, 以秒为单位,返回过期时间
     *
     * @param key 键
     * @return 过期时间
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 设置过期时间,单位: 秒
     *
     * @param key     键
     * @param timeout 过期时间
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置过期时间
     *
     * @param key      键
     * @param timeout  过期时间
     * @param timeUnit 时间单元
     */
    public void expire(String key, long timeout, TimeUnit timeUnit) {
        redisTemplate.expire(key, timeout, timeUnit);
    }

    /**
     * 增加key1次
     *
     * @param key 键
     */
    public void incr(String key) {
        redisTemplate.opsForValue().increment(key);
    }

    /**
     * 增加<ref>num</ref>次
     *
     * @param key 键
     * @param num 值
     */
    public void incyByNum(String key, long num) {
        redisTemplate.opsForValue().increment(key, num);
    }

    /**
     * 减少key1次
     *
     * @param key 键
     */
    public void decr(String key) {
        redisTemplate.opsForValue().decrement(key);
    }

    public void decrByNum(String key, long num) {
        redisTemplate.opsForValue().decrement(key, num);
    }

    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    public void del(String key) {
        redisTemplate.delete(key);
    }

    // string 操作

    /**
     * 设置值
     *
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 设置值和过期时间
     *
     * @param key
     * @param value
     * @param timeout
     */
    public void setAndExpire(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 获取值
     *
     * @param key
     * @return
     */
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 获取一段范围内的字符
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public String getRange(String key, long start, long end) {
        return redisTemplate.opsForValue().get(key, start, end);
    }

    /**
     * 设置不存在的键
     *
     * @param key
     * @param value
     */
    public void setn(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 批量查询, 对应mget
     *
     * @param keys
     * @return
     */
    public List<String> mget(List<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }


    // hash 操作

    public void hset(String key, String filed, String value) {
        redisTemplate.opsForHash().put(key, filed, value);
    }

    public String hget(String key, String filed) {
        return (String) redisTemplate.opsForHash().get(key, filed);
    }

    public void hdel(String key, Object... filed) {
        redisTemplate.opsForHash().delete(key, filed);
    }

    public Map<Object, Object> hgetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // list 操作

    public void lpush(String key, String value) {
        redisTemplate.opsForList().leftPush(key, value);
    }

    public void lpush(String key, String... value) {
        redisTemplate.opsForList().leftPushAll(key, value);
    }

    public void rpush(String key, String value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    public void rpush(String key, String... value) {
        redisTemplate.opsForList().rightPushAll(key, value);
    }

    public String lpop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    public String rpop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }
}

思维导图

redission isHeldByCurrentThread 失败 redis is not empty_数据库_08