Redis:高性能的内存数据库

发表时间: 2023-09-06 15:21

1、什么是Redis?

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

Redis 与其他 key - value 缓存产品相比有以下三个特点:

  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis 支持数据的备份,即 master-slave 模式的数据备份。m

Redis 优势:

  • 性能极高:Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。
  • 丰富的数据类型:Redis 支持二进制案例的 Strings,Lists,Hashes,Sets 及 Ordered Sets 数据类型操作。
  • 原子:Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。
  • 丰富的特性:Redis 还支持 publish/subscribe,通知,key 过期等等特性。

Redis 与其他 key-value 存储有什么不同?

  • Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
  • Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis 可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

2、Redis的数据类型

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set(有序集合)。在 2.8.9 版本之后添加了 HyperLogLog 结构,在3.2版本之后增加了GEO的数据结构,主要用于存储地理位置信息结构,在5.0 版本新增加了Redis Stream的数据结构(Pub/Sub)。

3、Redis 是单进程单线程的?

Redis 是单进程单线程的,Redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

4、Redis的持久化机制是什么?各自的优缺点?

Redis提供两种持久化机制 RDB 和 AOF 机制:

RDB(Redis DataBase)持久化方式:是指用数据集快照的方式半持久化模式记录 Redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

优点:

  • 只有一个文件 dump.rdb,方便持久化。
  • 容灾性好,一个文件可以保存到安全的磁盘。
  • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis的高性能。
  • 相对于数据集大时,比 AOF 的启动效率更高。

缺点:

  • 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候

AOF(Append-only file)持久化方式:是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储保存为 aof 文件。

优点:

  • 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
  • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
  • AOF 机制的 rewrite 模式。AOF 文件没被 rewrite之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)

缺点:

  • AOF 文件比 RDB 文件大,且恢复速度慢。
  • 数据集大的时候,比 RDB 启动效率低。

5、Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson,SpringBoot默认是lettuce。

1、Jedis

是Redis的Java实现的客户端。支持基本的数据类型如:String、Hash、List、Set、Sorted Set。

特点:使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作。Jedis客户端实例不是线程安全的,需要通过连接池来使用Jedis。

2、Redisson

优点点:分布式锁,分布式集合,可通过Redis支持延迟队列。

3、 Lettuce

用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。

一、键值设计

1. key名设计

(1)【建议】: 可读性和可管理性

以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id

ugc:video:1

(2)【建议】:简洁性

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

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

(3)【强制】:不要包含特殊字符

反例:包含空格、换行、单双引号以及其他转义字符

详细解析

2. value设计

(1)【强制】:拒绝bigkey(防止网卡流量、慢查询)

string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。

反例:一个包含200万个元素的list。

非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法删除方法

详细解析

(2)【推荐】:选择适合的数据类型。

例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)

反例:

set user:1:name tomset user:1:age 19set user:1:favor football

正例:

hmset user:1 name tom age 19 favor football

3.【推荐】:控制key的生命周期,redis不是垃圾桶。

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。

二、命令使用

1.【推荐】 O(N)命令关注N的数量

例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。

2.【推荐】:禁用命令

禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。

3.【推荐】合理使用select

redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。

4.【推荐】使用批量操作提高效率

原生命令:例如mget、mset。

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

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

注意两者不同:

  1. 原生是原子操作,pipeline是非原子操作。
  2. pipeline可以打包不同的命令,原生做不到
  3. pipeline需要客户端和服务端同时支持。

5.【建议】Redis事务功能较弱,不建议过多使用

Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)

6.【建议】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"。

7.【建议】必要情况下使用monitor命令时,要注意不要长时间使用。

三、客户端使用

1.【推荐】

避免多个应用使用一个Redis实例

正例:不相干的业务拆分,公共数据做服务化。

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();

}

下面是JedisPool优化方法的文章:

  • Jedis常见异常汇总
  • JedisPool资源池优化

3.【建议】

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

4.【推荐】

设置合理的密码,如有必要可以使用SSL加密访问(阿里云Redis支持)

5.【建议】

根据自身业务类型,选好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只响应读操作。

四、相关工具

1.【推荐】:数据同步

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

2.【推荐】:big key搜索

redis大key搜索工具

3.【推荐】:热点key寻找(内部实现使用monitor,所以建议短时间使用)

facebook的redis-faina

阿里云Redis已经在内核层面解决热点key问题,欢迎使用。

五 附录:删除bigkey

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

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);

}

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);

}

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);

}

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);

}