RedisRedis使用总结以及线程雪崩解决方案

125 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Redis数据类型

Redis存储是Key-Value结构数据,其中key是字符串类型,value有5种常用的数据类型

字符串 String 哈希 hash 适合存储 对象类型 列表 list
集合 set 有序集合 sorted set /zet

Redis 常用的5种数据类型 在这里插入图片描述

一、字符串String常用命令

1、SET key value 2、GET key 3、SETEX key seconds values 适合用来做短信验证码的应用 4、SETNX key value 只有在key 不存在的时候 设置key的 值,可以用来做分布式锁,但是值得注意的是,当用完之和要把key 释放掉。不然会阻塞其他节点访问;

例如发优惠券,多个节点发优惠券,谁先那到这个锁谁先发,只有一个人能那到,拿到后要释放锁。

二、Hash 常用命令

Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:

  • HSET key field value 将哈希表 key 中的字段 field 的值设为 value 例如
    hset 武汉 青龙区 500亿 hset 武汉 黄陂区 600亿;

        hset   人民日报   粉丝   1000w
        hset   人民日报   关注    20w;
       
    
  • HGET key field 获取存储在哈希表中指定字段的值

  • HDEL key field 删除存储在哈希表中的指定字段

  • HKEYS key 获取哈希表中所有字段

  • HVALS key 获取哈希表中所有值

  • HGETALL key 获取在哈希表中指定 key 的所有字段和值

在这里插入图片描述

三、list 支持 队列模型FIFO(先进先出)和栈模型(先进后出),双端阻塞队列(取不到数据会等待)

  • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
  • LRANGE key start stop 获取列表指定范围内的元素
  • RPOP key 移除并获取列表最后一个元素
  • LLEN key 获取列表长度
  • BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止

四 Set常用操作命令

Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:

  • SADD key member1 [member2] 向集合添加一个或多个成员

  • SMEMBERS key 返回集合中的所有成员

  • SCARD key 获取集合的成员数

  • SINTER key1 [key2] 返回给定所有集合的交集

  • SUNION key1 [key2] 返回所有给定集合的并集

  • SDIFF key1 [key2] 返回给定所有集合的差集 -在这里插入图片描述

  • SREM key member1 [member2] 移除集合中一个或多个成员

其中交集可以做热点数据,把多个用户的收藏或者 用户购物车数据做一个交集,当人具体问题具体分析

set集合可以做元素的交叉集运算

在这里插入图片描述

五、有序集合sorted set操作命令

Redis sorted set 有序集合是 string 类型元素的集合,且不允许重复的成员。每个元素都会关联一个double类型的分数(score) 。redis正是通过分数来为集合中的成员进行从小到大排序。有序集合的成员是唯一的,但分数却可以重复。

常用命令:

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的 分数
  • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
  • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
  • ZREM key member [member ...] 移除有序集合中的一个或多个成员

例如 zadd key hero 10 貂蝉 11 吕布 9 鲁班 在这里插入图片描述

六、通用命令

Redis中的通用命令,主要是针对key进行操作的相关命令:

  • KEYS pattern 查找所有符合给定模式( pattern)的 key

例如 keys user 会把所有user前缀的key查询出来*

  • EXISTS key 检查给定 key 是否存在

  • TYPE key 返回 key 所储存的值的类型

  • TTL key 返回给定 key 的剩余生存时间(TTL, time to live),以秒为单位

  • DEL key 该命令用于在 key 存在时候删除 key

七、JAVA 操作 redis

客户端有 Jedis 对标 jdbc 操作 一般不用

Lettuce

Redisson

7.1 Jedis

依赖

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

用法:

package com.ligong.test;

import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.util.Set;

/**
 * 使用Jedis操作Redis
 */
public class JedisTest {

    @Test
    public void testRedis(){
        //1 获取连接
        Jedis jedis = new Jedis("localhost",6379);
        
        //2 执行具体的操作
        jedis.set("username","xiaoming");

        String value = jedis.get("username");
        System.out.println(value);

        //jedis.del("username");

        jedis.hset("myhash","addr","bj");
        String hValue = jedis.hget("myhash", "addr");
        System.out.println(hValue);

        Set<String> keys = jedis.keys("*");
        for (String key : keys) {
            System.out.println(key);
        }

        //3 关闭连接
        jedis.close();
    }
}

7.2 Lettuce

7.2.1、Spring整合Redis

引入依赖

<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-redis</artifactId>
	<version>2.4.8</version>
</dependency>

7.2.2、SpringBoot整合Redis

其本质上其实还是Jedis,只不过进行了模版封装

第一步:引入SpringBoot 整合Redis起步依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步:application.yml配置

spring:
  application:
    name: springdataredis_demo
  #Redis相关配置
  redis:
    host: localhost
    port: 6379
    #password: 123456
    database: 0 #操作的是0号数据库
    jedis:
      #Redis连接池配置
      pool:
        max-active: 8 #最大连接数
        max-wait: 1ms #连接池最大阻塞等待时间
        max-idle: 4 #连接池中的最大空闲连接
        min-idle: 0 #连接池中的最小空闲连接

解释说明:

spring.redis.database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。

可以通过修改Redis配置文件来指定数据库的数量。

第三步:编写启动类 略

应用

其中RedisTemplate 模版,在SpringBoot启动时候,如果缺失RedisTemplate,则SpringBoot会自动注入,并且各种参数都会自动从yaml中自动读取。

如果不手动设置序列化,那么会默认设置jdk序列化。 默认:JdkSerializationRedisSerializer

不同的要求,那么序列化不同的key和值。

不建议Value 进行序列设置。

第四步:提供配置类

package com.itheima.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.setConnectionFactory(connectionFactory);

        return redisTemplate;
    }

}

解释说明:

当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别

第五步 提供测试类

package com.ligong.test;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringDataRedisTest {

    @Autowired
    private RedisTemplate redisTemplate;
    
}
5.1 操作字符串类型数据
/**
 * 操作String类型数据
*/
@Test
public void testString(){
    //存值
    redisTemplate.opsForValue().set("city123","beijing");

    //取值
    String value = (String) redisTemplate.opsForValue().get("city123");
    System.out.println(value);

    //存值,同时设置过期时间
    redisTemplate.opsForValue().set("key1","value1",10l, TimeUnit.SECONDS);

    //存值,如果存在则不执行任何操作
    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("city1234", "nanjing");
    System.out.println(aBoolean);
}
5.2 操作哈希类型数据
/**
 * 操作Hash类型数据
*/
@Test
public void testHash(){
    HashOperations hashOperations = redisTemplate.opsForHash();

    //存值
    hashOperations.put("002","name","xiaoming");
    hashOperations.put("002","age","20");
    hashOperations.put("002","address","bj");

    //取值
    String age = (String) hashOperations.get("002", "age");
    System.out.println(age);

    //获得hash结构中的所有字段
    Set keys = hashOperations.keys("002");
    for (Object key : keys) {
        System.out.println(key);
    }

    //获得hash结构中的所有值
    List values = hashOperations.values("002");
    for (Object value : values) {
        System.out.println(value);
    }
}
5.3 操作列表类型数据
/**
 * 操作List类型的数据
*/
@Test
public void testList(){
    ListOperations listOperations = redisTemplate.opsForList();

    //存值
    listOperations.leftPush("mylist","a");
    listOperations.leftPushAll("mylist","b","c","d");

    //取值
    List<String> mylist = listOperations.range("mylist", 0, -1);
    for (String value : mylist) {
        System.out.println(value);
    }

    //获得列表长度 llen
    Long size = listOperations.size("mylist");
    int lSize = size.intValue();
    for (int i = 0; i < lSize; i++) {
        //出队列
        String element = (String) listOperations.rightPop("mylist");
        System.out.println(element);
    }
}
5.4 操作集合类型数据
/**
 * 操作Set类型的数据
*/
@Test
public void testSet(){
    SetOperations setOperations = redisTemplate.opsForSet();

    //存值
    setOperations.add("myset","a","b","c","a");

    //取值
    Set<String> myset = setOperations.members("myset");
    for (String o : myset) {
        System.out.println(o);
    }

    //删除成员
    setOperations.remove("myset","a","b");

    //取值
    myset = setOperations.members("myset");
    for (String o : myset) {
        System.out.println(o);
    }

}
5.5 操作有序集合类型数据
/**
 * 操作ZSet类型的数据
*/
@Test
public void testZset(){
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();

    //存值
    zSetOperations.add("myZset","a",10.0);
    zSetOperations.add("myZset","b",11.0);
    zSetOperations.add("myZset","c",12.0);
    zSetOperations.add("myZset","a",13.0);

    //取值
    Set<String> myZset = zSetOperations.range("myZset", 0, -1);
    for (String s : myZset) {
        System.out.println(s);
    }

    //修改分数
    zSetOperations.incrementScore("myZset","b",20.0);

    //取值
    myZset = zSetOperations.range("myZset", 0, -1);
    for (String s : myZset) {
        System.out.println(s);
    }

    //删除成员
    zSetOperations.remove("myZset","a","b");

    //取值
    myZset = zSetOperations.range("myZset", 0, -1);
    for (String s : myZset) {
        System.out.println(s);
    }
}
5.6 通用操作
/**
 * 通用操作,针对不同的数据类型都可以操作
*/
@Test
public void testCommon(){
    //获取Redis中所有的key
    Set<String> keys = redisTemplate.keys("*");
    for (String key : keys) {
        System.out.println(key);
    }

    //判断某个key是否存在
    Boolean itcast = redisTemplate.hasKey("itcast");
    System.out.println(itcast);

    //删除指定key
    redisTemplate.delete("myZset");

    //获取指定key对应的value的数据类型
    DataType dataType = redisTemplate.type("myset");
    System.out.println(dataType.name());

}

八、线上操作Redis导致雪崩宕机情况。

导火索:线上频发报警,网关频繁转发超时,系统很多服务响应时间过长,服务cpu过载,数据库死锁等不正常现象

问题排查:目标范围缩小在近两日上线功能,并锁定在新上线的操作redis缓存功能上。

问题复现:将功能在开发环境测试,由于访问量不足并未发现异常。

将某需求下线后,线上环境得以好转。

分析代码:发现如下

  /**
     * Del.
     *
     * @param keys the keys
     */
    public static void del(String... keys) {
        try (Jedis jedis = getJedis()) {
            jedis.del(Arrays.stream(keys).map(v -> key(v)).collect(Collectors.toList()).toArray(new String[]{}));
        } catch (Exception e) {
            log.error("Redis error:{}", ExceptionTools.getExceptionStackTrace(e));
            throw new RuntimeException(e);
        }
 
    }
 
    /**
     * del by pattern
     *
     * @param pattern prefix*
     */
    public static Long del(String pattern) {
        try (Jedis jedis = getJedis()) {
            Set<String> keys = jedis.keys(key(pattern));
            if (CollectionUtils.isNotEmpty(keys)) {
                return jedis.del(keys.toArray(new String[0]));
            } else {
                return 0L;
            }
        } catch (Exception e) {
            log.error("Redis error:{}", ExceptionTools.getExceptionStackTrace(e));
            throw new RuntimeException(e);
        }
    }

事故原因:工程师执行redis keys * 导致环境濒临宕机! 由于及时发现,虽然未造成重大事故,但实属本年度PO级特大事故:

由于工程师直接操作上线redis,执行:

keys * wxdb(此处省略)cf8*

新增的单参数的del方法覆盖了多参数的del方法,所有调用del的地方都走单参del方法,然而此方法内含有keys命令。

redis是单线程服务,这样的命令,导致redis锁住,导致CPU飙升,引起所有支付链路卡住,等十几秒结束后,所有的请求流量全部挤压到了rds数据库中,使数据库产生了雪崩效应,发生了数据库宕机事件。

由于之前验收环境缓存量小,未发现此影响其他服务响应延迟的问题。

运维表示之后会逐步收回运维部各项权限!

二、一条铁律

在业内,redis开发规范中有一条铁律如下所示:

线上Redis禁止使用Keys正则匹配操作!

然而大家都知道,却一直忘记,所以事故会不断的发生。 下面讲一讲在线上执行正则匹配操作,引起缓存雪崩,最终数据库宕机的原因。

三、深度分析

1、redis是单线程的,其所有操作都是原子的,不会因并发产生数据异常;

2、使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用);

有上面两句作铺垫,原因就显而易见了!

运维人员进行keys *操作,该操作比较耗时,又因为redis是单线程的,所以redis被锁住; 此时QPS比较高,又来了几万个对redis的读写请求,因为redis被锁住,所以全部Hang在那; 因为太多线程Hang在那,CPU严重飙升,造成redis所在的服务器宕机; 所有的线程在redis那取不到数据,一瞬间全去数据库取数据,数据库就宕机了; 需要注意的是,同样危险的命令不仅有keys *,还有以下几组:

因此,一个合格的redis运维或者开发,应该懂得如何禁用上面的命令。所以我一直觉得出现新闻中那种情况的原因,一般是人员的水平问题。

四、怎么禁用这些命令呢?

就是在redis.conf中,在SECURITY这一项中,我们新增以下命令:

另外,对于FLUSHALL命令,需要设置配置文件中appendonly no,否则服务器是无法启动。

注意了,上面的这些命令可能有遗漏,大家可以查官方文档。除了Flushdb这类和redis安全隐患有关的命令意外,但凡发现时间复杂度为O(N)的命令,都要慎重,不要在生产上随便使用。例如hgetall、lrange、smembers、zrange、sinter等命令,它们并非不能使用,但这些命令的时间复杂度都为O(N),使用这些命令需要明确N的值,否则也会出现缓存宕机。

五、改良建议

业内建议使用scan命令来改良keys和SMEMBERS命令:

Redis2.8版本以后有了一个新命令scan,可以用来分批次扫描redis记录,这样肯定会导致整个查询消耗的总时间变大,但不会影响redis服务卡顿,影响服务使用。