1.字符串(String)
字符串类型是Redis最基础的数据结构,字符串类型可以是JSON
、XML
甚至是二进制的图片等数据,但是最大值不能超过512MB。
1.1 内部编码
Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。
字符串类型的内部编码有3种:
-
int:8个字节的长整型。
-
embstr:小于等于39个字节的字符串。
-
raw:大于39个字节的字符串。
1.2.适用场景
1.2.1 缓存
在web服务中,使用MySQL作为数据库,Redis作为缓存。由于Redis具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web端的大多数请求都是从Redis中获取的数据,如果Redis中没有需要的数据,则会从MySQL中去获取,并将获取到的数据写入redis。
1.2.2 计数器
Redis中有一个字符串相关的命令incr key
,incr
命令对值做自增操作,返回结果分为以下三种情况:
-
值不是整数,返回错误
-
值是整数,返回自增后的结果
-
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 key
,incr
命令对值做自增操作
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。
-
使用批量操作提高效率:
-
原生命令是原子操作,pipeline是非原子操作
-
pipeline可以打包不同的命令,原生不支持
-
pipeline需要客户端和服务端同时支持
-
原生命令:如mget、mset。
-
非原生命令:可以使用pipeline提高效率。
-
但要注意控制一次批量操作的元素个数(例如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);
}