Redis 适用场景

1.字符串(String)

        字符串类型是Redis最基础的数据结构,字符串类型可以是JSONXML甚至是二进制的图片等数据,但是最大值不能超过512MB。

1.1 内部编码

Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。

字符串类型的内部编码有3种:

  1. int:8个字节的长整型。

  2. embstr:小于等于39个字节的字符串。

  3. raw:大于39个字节的字符串。

1.2.适用场景

1.2.1 缓存

在web服务中,使用MySQL作为数据库,Redis作为缓存。由于Redis具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web端的大多数请求都是从Redis中获取的数据,如果Redis中没有需要的数据,则会从MySQL中去获取,并将获取到的数据写入redis。

1.2.2 计数器

Redis中有一个字符串相关的命令incr keyincr命令对值做自增操作,返回结果分为以下三种情况:

  • 值不是整数,返回错误

  • 值是整数,返回自增后的结果

  • key不存在,默认键为0,返回1

比如文章的阅读量,视频的播放量等等都会使用redis来计数,每播放一次,对应的播放量就会加1,同时将这些数据异步存储到数据库中达到持久化的目的。

1.2.3 共享Session

在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致session不同步的问题,假如一个用来获取用户信息的请求落在A服务器上,获取到用户信息后存入session。下一个请求落在B服务器上,想要从session中获取用户信息就不能正常获取了,因为用户信息的session在服务器A上,为了解决这个问题,使用redis集中管理这些session,将session存入redis,使用的时候直接从redis中获取就可以了。

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

开启RedisSession配置

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {

}

Controller 部分代码如下:

@RestController
public class TestController {

	@GetMapping("/set-session")
	public Object writeSession(String sessionVal, HttpSession httpSession) {
	System.out.println("Param 'sessionVal' = " + sessionVal);
	httpSession.setAttribute("sessionVal", sessionVal);
	return sessionVal;
	}

	@GetMapping("/get-session")
	public Object readSession(HttpSession httpSession) {
	Object obj = httpSession.getAttribute("sessionVal");
	System.out.println("'sessionVal' in Session = " + obj);
	return obj;
	}

}

通过 Nginx 做负载均衡
分别在 8080和 8081两个端口启动 Server,然后通过 Nginx 配置负载均衡,配置如下:

http {

    upstream app_server {
		server 127.0.0.1:8080;
		server 127.0.0.1:8081;
	}

	server {
		listen 80;
		location / {
			proxy_pass http://app_server;
		}
	}
}

1.2.4 限速、限流 

为了安全考虑,有些网站会对IP进行限制,限制同一IP在一定时间内访问次数不能超过n次。

incr key,incr命令对值做自增操作

1.2.5 处理幂等性

为了接口、消息等处理幂等性,可以借助该缓存做缓冲等,有时间限制,若长期的幂等性,则要借助数据库完成。

1.2.6分布式锁

String 类型setnx方法,只有不存在时才能添加成功,返回true

1.2.7分布式ID

incr keyincr命令对值做自增操作

incrby key increment 将 key 所储存的值加上给定的增量值(increment) 。

1.2.8 用户上线统计

String类型的bitcount(1.6.6的bitmap数据结构介绍)

如统计车辆今日是否上线、用户上线日期等

#SETBIT key offset value
#对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit),value不填写时,默认为0。
#用户编号1上线在2021年08月11日
setbit online:users:20210811 01 1
#用户编号100上线在2021年08月11日
setbit online:users:20210811 100 1

字符是以8位二进制存储的

支持按位与、按位或等等操作

BITOP AND dest key key[key...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。       
BITOP ORd est key key[key...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。 
BITOP XOR dest key key[key...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。 
BITOP NOT dest key key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

1.2.9 限时任务

如商家限时优惠、手机验证码、接口token等只在一段时间内可用。

2.哈希(Hash)

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。

Redis 中每个 hash 可以存储 2^32-1 键值对(40多亿)。

2.1 内部编码

哈希类型的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)同时所有值都小于hash-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。

  • hashtable(哈希表):当ziplist不能满足要求时,会使用hashtable。

2.2 使用场景

2.2.1.对象实体

如用户表,方便后期针对其中某一属性进行修改移除

2.2.2.购物车

  • ey:用户id;field:商品id;value:商品数量。

  • +1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。

注: string可以实现的工作,使用hash同样可以完成。且比字符串更加明显直观。

3. 列表(List)

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),一个列表最多可以包含2^32-1 个元素 (4294967295, 每个列表超过40亿个元素)。

3.1 内部编码

列表的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于list-max-ziplist-entries配置(默认512个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。

  • linkedlist(链表):当ziplist不能满足要求时,会使用linkedlist。

3.2 使用场景

3.2.1 消息队列

列表用来存储多个有序的字符串,既然是有序的,那么就满足消息队列的特点。使用lpush+rpop或者rpush+lpop实现消息队列。除此之外,redis支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。

3.2.2 栈

由于列表存储的是有序字符串,满足队列的特点,也就能满足栈先进后出的特点,使用lpush+lpop或者rpush+rpop实现栈。

3.2.3 文章列表

因为列表的元素不但是有序的,而且还支持按照索引范围获取元素。因此我们可以使用命令lrange key 0 9分页获取文章列表。

3.2.4.时间线

4.集合(Set)

Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。集合对象的编码可以是 intset 或者 hashtable。Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。集合中最大的成员数为 2^32-1 (4294967295, 每个集合可存储40多亿个成员)。

4.1 内部编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,redis会选用intset来作为集合的内部实现,从而减少内存的使用。

  • hashtable(哈希表):当intset不能满足要求时,会使用hashtable。

4.2 使用场景

4.2.1 用户标签

4.2.2 抽奖功能

集合有两个命令支持获取随机数,分别是:

  • 随机获取count个元素,集合元素个数不变

srandmember key [count]
  • 随机弹出count个元素,元素从集合弹出,集合元素个数改变

spop key [count]

用户点击抽奖按钮,参数抽奖,将用户编号放入集合,然后抽奖,分别抽一等奖、二等奖,如果已经抽中一等奖的用户不能参数抽二等奖则使用spop,反之使用srandmember

5.有序集合(sorted set)

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 2^32-1(4294967295, 每个集合可存储40多亿个成员)。

5.1 内部编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当有序集合的元素个数小于list-max-ziplist-entries配置(默认128个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。

  • skiplist(跳跃表):当不满足ziplist的要求时,会使用skiplist。

5.2 使用场景

5.2.1 排行榜

5.2.2 延迟消息队列

下单系统,下单后需要在15分钟内进行支付,如果15分钟未支付则自动取消订单。将下单后的十五分钟后时间作为score,订单作为value存入redis,消费者轮询去消费,如果消费的大于等于这笔记录的score,则将这笔记录移除队列,取消订单。

6.规范建议

6.1. key名设计

6.1.1【建议】

  •  可读性和可管理性

以业务名(或数据库名)为前缀(防止key冲突),必须使用冒号分隔,便于RDM查看,比如应用名称:租户号:DD_CODE。

APPKEY:TENANT_CODE:DD_CODE
  • 简洁性

保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:

user:{uid}:friends:messages:{mid}简化为u:{uid}:fr:m:{mid}。

6.1.2【强制】

  • 长度50个字符以内:不要包含空格、换行,引号和一些转义字符

  • 控制key的总数量:redis实例包含的键个数建议控制在 1 千万内,单实例的键个数过大,可能导致过期键的回收不及时。

6.2. value设计

6.2.1【强制】

  • 拒绝bigkey(防止网卡流量、慢查询)

string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法

  • 控制key生命周期,redis不是垃圾站

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。如果业务强制需求不过期,请说明具体原因。

6.2.2【推荐】

  • 选择适合的数据类型。

6.3命令使用

6.3.1.【建议】

  • Redis事务功能较弱:不建议过多使用,Redis事务功能不支持回滚,cluster 要求事务操作的key必须在一个slot上面。

  •  Redis集群版本在使用Lua上有特殊要求

  • 所有key都应该由 KEYS 数组来传递:redis.call/pcall 里面调用的redis命令,key的位置,必须是KEYS array, 否则直接返回error,-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array

  • 所有key,必须在1个slot上,否则直接返回error, “-ERR eval/evalsha command keys must in same slot”

  • 必要情况下使用Monitor命令时,要注意不要长时间使用,造成缓冲区溢出,尽而内存抖动

6.3.2【推荐】

  • 禁用命令:禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan

  • O(N)命令关注N的数量:例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。的方式渐进式处理。

  • 避免使用select:使用登录上去默认的db0;redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。哨兵模式中不建议使用多db,毕竟集群模式已经不能使用多db。

  • 使用批量操作提高效率:

  1. 原生命令是原子操作,pipeline是非原子操作

  2. pipeline可以打包不同的命令,原生不支持

  3. pipeline需要客户端和服务端同时支持

  4. 原生命令:如mget、mset。

  5. 非原生命令:可以使用pipeline提高效率。

  6. 但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。

6.4 客户端使用

 6.4.1【建议】

  • 高并发下建议客户端添加熔断功能(例如netflix hystrix)

  • 设置合理的密码,如有必要可以使用SSL加密访问

  •  根据自身业务类型,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间。

    默认策略是volatile-lru,即超过最大内存后,在过期键中使用lru算法进行key的剔除,保证不过期 数据不被删除,但是可能会出现OOM问题。

    其他策略如下:

    allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
    allkeys-random:随机删除所有键,直到腾出足够空间为止。
    volatile-random: 随机删除过期键,直到腾出足够空间为止。
    volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction
    策略。
    noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM
    command not allowed when used memory",此时Redis只响应读操作。
    

6.4.2【推荐】

  • 使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式:
//执行命令如下:
Jedis jedis = null;
try {
    jedis = jedisPool.getResource();
    //具体的命令
    jedis.executeCommand()
} catch (Exception e) {
    logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
    //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
    if (jedis != null)
    jedis.close();
}
  • 设置合理的密码,如有必要可以使用SSL加密访问

6.5.合理使用

6.5.1【建议】

  • 根据业务场景合理使用不同的数据结构类型

目前Redis支持的数据库结构类型较多:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set), Bitmap, HyperLogLog和地理空间索引(geospatial)等,需要根据业务场景选择合适的类型,常见的如:String可以用作普通的K-V、计数类;Hash可以用作对象如商品、经纪人等,包含较多属性的信息;List可以用作消息队列、粉丝/关注列表等;Set可以用于推荐;Sorted Set可以用于排行榜等!

6.5.2【推荐】

  • 冷热数据分离,不要将所有数据全部都放到Redis中

虽然Redis支持持久化,但是Redis的数据存储全部都是在内存中的,成本昂贵。建议根据业务只将高频热数据存储到Redis中【QPS大于5000】,对于低频冷数据可以使用MySQL/ElasticSearch/MongoDB等基于磁盘的存储方式,不仅节省内存成本,而且数据量小在操作时速度更快、效率更高!

  • 不同的业务数据要分开存储

不要将不相关的业务数据都放到一个Redis实例中,建议新业务申请新的单独实例。因为Redis为单线程处理,独立存储会减少不同业务相互操作的影响,提高请求响应速度;同时也避免单个实例内存数据量膨胀过大,在出现异常情况时可以更快恢复服务!

  • 存储的Key一定要设置超时时间

如果应用将Redis定位为缓存Cache使用,对于存放的Key一定要设置超时时间!因为若不设置,这些Key会一直占用内存不释放,造成极大的浪费,而且随着时间的推移会导致内存占用越来越大,直到达到服务器内存上限!另外Key的超时长短要根据业务综合评估,而不是越长越好!

  • 对于必须要存储的大文本数据一定要压缩后存储

对于大文本【超过500字节】写入到Redis时,一定要压缩后存储!大文本数据存入Redis,除了带来极大的内存占用外,在访问量高时,很容易就会将网卡流量占满,进而造成整个服务器上的所有服务不可用,并引发雪崩效应,造成各个系统瘫痪!

  • 谨慎全量操作Hash、Set等集合结构

在使用HASH结构存储对象属性时,开始只有有限的十几个field,往往使用HGETALL获取所有成员,效率也很高,但是随着业务发展,会将field扩张到上百个甚至几百个,此时还使用HGETALL会出现效率急剧下降、网卡频繁打满等问题【时间复杂度O(N)】,此时建议根据业务拆分为多个Hash结构;或者如果大部分都是获取所有属性的操作,可以将所有属性序列化为一个STRING类型存储!同样在使用SMEMBERS操作SET结构类型时也是相同的情况!

6.5.3 【强制】

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

Redis是单线程处理,在线上KEY数量较多时,操作效率极低【时间复杂度为O(N)】,该命令一旦执行会严重阻塞线上其它命令的正常请求,而且在高QPS情况下会直接造成Redis服务崩溃!如果有类似需求,请使用scan命令代替!

6.6.相关工具

6.6.1【推荐】

  • 数据同步:redis间数据同步可以使用:redis-port

  • big key搜索:对于Redis主从版本可以通过scan命令进行扫描,对于集群版本提供了ISCAN命令进行扫描,命令规则 如下, 其中节点个数node可以通过info命令来获取到:

  • 热点key寻找(内部实现使用monitor,所以建议短时间使用,生产环境一般不建议使用)

6.7.删除bigkey

1. 下面操作可以使用pipeline加速。
2. redis 4.0已经支持key的异步删除,建议使用。

6.7.1. Hash删除: hscan + hdel

 public void delBigHash (String host,int port, String password, String
        bigHashKey) {
            Jedis jedis = new Jedis(host, port);
            if (password != null && !"".equals(password)) {
                jedis.auth(password);
            }
            ScanParams scanParams = new ScanParams().count(100);
            String cursor = "0";
            do {
                ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey,
                        cursor, scanParams);
                List<Entry<String, String>> entryList = scanResult.getResult();
                if (entryList != null && !entryList.isEmpty()) {
                    for (Entry<String, String> entry : entryList) {
                        jedis.hdel(bigHashKey, entry.getKey());
                    }
                }
                cursor = scanResult.getStringCursor();
            } while (!"0".equals(cursor));
            //删除bigkey
            jedis.del(bigHashKey);
}

6.7.2 List删除: ltrim

public void delBigList(String host, int port, String password, String
bigListKey) {
  Jedis jedis = new Jedis(host, port);
  if (password != null && !"".equals(password)) {
    jedis.auth(password);
  }
  long llen = jedis.llen(bigListKey);
  int counter = 0;
  int left = 100;
  while (counter < llen) {
    //每次从左侧截掉100个
    jedis.ltrim(bigListKey, left, llen);
    counter += left;
  }
  //最终删除key
  jedis.del(bigListKey);
}

6.7.3 Set删除: sscan + srem

 public void delBigSet(String host, int port, String password, String bigSetKey) {
            Jedis jedis = new Jedis(host, port);
            if (password != null && !"".equals(password)) {
                jedis.auth(password);
            }
            ScanParams scanParams = new ScanParams().count(100);
            String cursor = "0";
            do {
                ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor,
                        scanParams);
                List<String> memberList = scanResult.getResult();
                if (memberList != null && !memberList.isEmpty()) {
                    for (String member : memberList) {
                        jedis.srem(bigSetKey, member);
                    }
                }
                cursor = scanResult.getStringCursor();
            } while (!"0".equals(cursor));
            //删除bigkey
            jedis.del(bigSetKey);
}

6.7.4.SortedSet删除: zscan + zrem

public void delBigZset(String host, int port, String password, String
        bigZsetKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor,
                scanParams);
        List<Tuple> tupleList = scanResult.getResult();
        if (tupleList != null && !tupleList.isEmpty()) {
            for (Tuple tuple : tupleList) {
                jedis.zrem(bigZsetKey, tuple.getElement());
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    //删除bigkey
    jedis.del(bigZsetKey);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值