开发大系统必备技术之Redis技术学习与研究
1、【引子】
前段时间有朋友问了一个问题,是关于如何开发一套大系统的问题。这类大系统可以像Twitter, Youtube这样的规模。其中有一个技术点,就是Redis系统。
接下来我们就通过这篇文章来学习和探索一下Redis系统。
2、【Redis】
Redis(远程字典服务器)是一个在内存上构建的数据结构系统,它实现了一个分布式的内存中的键值数据库,它具有一定程度的持久性。Redis支持不同类型的抽象数据结构,如字符串、列表、映射、集、排序集、HyperLogLogs、位图、流和空间索引等。
该系统主要由Salvatore Sanfilippo开发,截止到2019年,由Redis实验室赞助,它是在BSD 3-clause许可证下发布的开源软件。
3、【历史】
Redis这个名字的意思是RemoteDictionary Server。Redis项目开始时,Redis的原始开发者Salvatore Sanfilippo,绰号antirez,他的意大利初创公司试图提高自己初创公司的可扩展性,于是开发了一个实时网络日志分析器。
但在使用传统的数据库系统扩展某些类型的工作负载时遇到了很大问题,之后,Sanfilippo开始用Tcl语言做了Redis的第一个概念验证版本,后来Sanfilippo把这个原型翻译成了C语言,并实现了第一个数据类型--list。
Sanfilippo在内部使用了几个星期并获得成功后,决定将其开源,在HackerNews上宣布了这个项目。这个项目开始得到了广泛的关注,在Ruby社区中更是如此,GitHub和Instagram是首批采用该项目的公司之一。
2010年3月,Sanfilippo被VMware录用。
2013年5月,Redis由Pivotal Software(VMware分拆出来的公司)赞助。
2015年6月,发展成为由Redis实验室赞助。
2018年10月,Redis 5.0版本发布,引入了Redis Stream---一种新的数据结构,可以在一个键上自动存储多个字段和字符串值,并以时间为基准的顺序自动存储。
4、【流行度】
- 根据DB-Engines的月度排名,Redis是最受欢迎的关键值数据库,根据用户评价,Redis也被评为用户满意度和市场占有率排名第4的NoSQL数据库,在容器中最受欢迎的NoSQL数据库。
- 在排名网站stackshare.io中,Redis被评为2015年NoSQL数据库第一名。
- 在2017年、2018年和2019年的Stack Overflow开发者调查中,Redis被评为最受喜爱的数据库。
5、【支持的语言】
从2.6版本开始,Redis采用Lua语言进行服务器端脚本编写。
许多编程语言在客户端都有Redis语言绑定,包括:ActionScript、C、C++、C#、Chicken、Clojure、CommonLisp、Crystal、D、Dart、Elixir、Erlang、Go、Haskell、Haxe、Io、Java、JavaScript(Node.js)、Julia、Lua、Objective-C、OCaml、Perl、PHP、Pure Data、Python、R、Racket、Ruby、Rust、Scala、Smalltalk、Swift和Tcl。
6、【设计理念】
Redis被认为是一个存储和缓存系统的集合体,它采用的设计思想是,数据总是从计算机主内存中进行修改和读取,同时也以不适合随机存取数据的格式存储在磁盘上,这些数据只能在系统重启后将数据重新组装回内存中。同时,与关系型数据库管理系统(RDBMS)相比,Redis提供的数据模型不同。用户命令不是描述数据库引擎要执行的查询,而是对给定的抽象数据类型执行的具体操作。因此,数据必须以一种适合于以后快速检索的方式进行存储,而不需要像数据库系统那样以二级索引、聚合或其他传统RDBMS的常见特性进行处理。Redis的实现中大量使用了fork系统调用,用来复制持有数据的进程,在这样的机制下,父进程持续为客户机提供服务,而子进程则负责在磁盘上创建一个数据副本。
6.1【数据类型】
- Redis是个键值映射系统。Redis与其他结构化存储系统的一个重要区别是,Redis不仅支持字符串,还支持抽象数据类型:
- 字符串的列表
- 字符串的集合(无重复无序元素的集合)。
- 排序的字符串集(按浮点分数排序的非重复元素的集合)。
- 散列表,其中的键和值是字符串。
- 自2014年4月Redis 2.8.9版本以来,HyperLogLogs用于近似集的贲度大小估计。
- 带消费组的分录流,允许你在一个单键上自动存储以时间为基础的序列,这个序列来自于多个字段和字符串值,自2018年10月Redis5.0开始,这项功能就可以使用了。
- 自Redis 3.2以来,通过实施geo-hash技术实现了地理空间数据的支持。
一个值的类型决定了该值可以进行哪些操作。Redis支持高级的、原子式的、服务器端的操作,如交集、联合、集与集之间的差分以及列表和集的排序等。
基于Redis模块API支持更多的数据类型,Redis模块RedisJSON把ECMA-404(JSON数据交换标准)作为一种原生数据类型。
6.2、【持久性】
Redis通常将整个数据集保存在内存中。2.4以下的版本可以被配置为使用他们所说的虚拟内存,其中部分数据集存储在磁盘上,但这个功能已经被废弃了。Redis中的持久化可以通过两种不同的方法来实现。首先是通过快照,即使用Redis RDB Dump文件格式,将数据集以二进制转储的形式从内存中异步传输到磁盘。另一种方法是通过日志化,即在后台进程中,将修改数据集的每个操作的记录添加到append-only文件(AOF)中。Redis可以在后台重写append-only文件,以避免日志的无限增长。日志是在1.1版本中引入的,通常被认为是比较安全的方法。
默认情况下,Redis至少每隔2秒向文件系统写入数据一次,如果需要,可以选择更多或更少的健壮选项。在默认设置的情况下,如果系统完全失效,只有指定的那几秒钟的数据会丢失。
6.3、【复制】
Redis支持主副本复制。任何Redis服务器的数据都可以复制出任意数量的副本。这使得Redis可以实现单根复制树。Redis副本被配置为可写入的,允许在实例之间有意和无意的不一致性。发布/订阅功能可以在复制体的客户端订阅一个通道,并在复制树上的任何地方接收发布到主服务器的消息。复制机制对于读取的可扩展性或数据备份是有用的。
6.4、【性能】
当不需要数据的持久性时,Redis的内存内特性使其与数据库系统相比,在性能处理上有很多优势。因为传统的关系型数据库系统在考虑事务提交之前,会将每一个变化写到磁盘上。
Redis作为一个单进程运行,在重写AOF(append-only文件)时是单线程或双线程的。因此,单个Redis实例不能进行并行任务,比如存储过程等。
6.5、【集群】
Redis在2015年4月发布了3.0版本,引入了集群,集群规范实现了Redis命令子集:所有的单键操作都是可用的,多键操作(与联合和交叉相关的命令)仅限于属于同一节点上的键。
与数据库选择相关的操作是不可用的。
一个Redis集群可以扩展到1000个节点,可以实现 "可接受的 "写安全,并能够在某些节点失效时继续工作。
7、【代码案例】
下面我们来看些例子。
7.1、【Jedis】
Jedis是一个非常小巧灵活的Redisjava客户端。
Jedis的设计是为了方便使用。
Jedis完全兼容Redis2.8.x、3.x.x及以上版本*。
7.1.1、【配置】
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>x.x.x</version>
</dependency>
7.1.2、【安装】
你需要安装并启动最新版本的Redis。
Jedis jedis = new Jedis();
默认的构造函数就可以正常工作,除非你用的是非默认端口或在远程机器上启动了服务,在这种情况下,你可以通过在构造函数中传递正确的参数值来正确配置。
7.1.3、【数据类型案例】
7.1.3.1、【字符串】
字符串是最基本的Redis值的一种,当你需要持久化简单的key-value数据类型时,字符串很有用。
jedis.set("events/city/rome", "32,15,223,828");
String cachedResponse = jedis.get("events/city/rome");
7.1.3.2、【列表】
Redis列表是简单的字符串列表,按插入顺序排序,使其成为实现消息队列等的理想工具。
jedis.lpush("queue#tasks", "firstTask");
jedis.lpush("queue#tasks", "secondTask");
String task = jedis.rpop("queue#tasks");
7.1.3.3、【集合】
Redis集是一个无序的字符串集合,可以用来排除重复的成员。
jedis.sadd("nicknames", "nickname#1");
jedis.sadd("nicknames", "nickname#2");
jedis.sadd("nicknames", "nickname#1");
Set<String> nicknames = jedis.smembers("nicknames");
boolean exists = jedis.sismember("nicknames", "nickname#1");
7.1.3.4、【哈希】
Redis Hashes是字符串字段和字符串值之间的映射。
jedis.hset("user#1", "name", "Peter");
jedis.hset("user#1", "job", "politician");
String name = jedis.hget("user#1", "name");
Map<String, String> fields = jedis.hgetAll("user#1");
String job = fields.get("job");
7.1.3.5、【有序的集合】
Map<String, Double> scores = new HashMap<>();
scores.put("PlayerOne", 3000.0);
scores.put("PlayerTwo", 1500.0);
scores.put("PlayerThree", 8200.0);
scores.entrySet().forEach(playerScore -> {
jedis.zadd(key, playerScore.getValue(), playerScore.getKey());
});
String player = jedis.zrevrange("ranking", 0, 1).iterator().next();
long rank = jedis.zrevrank("ranking", "PlayerOne");
7.1.4、【事务】
事务保证了原子性和线程安全操作。
String friendsPrefix = "friends#";
String userOneId = "4352523";
String userTwoId = "5552321";
Transaction t = jedis.multi();
t.sadd(friendsPrefix + userOneId, userTwoId);
t.sadd(friendsPrefix + userTwoId, userOneId);
t.exec();
7.1.5、【管线】
当我们需要发送多条命令时,我们可以将多条命令打包成一条请求,通过使用流水线来节省连接开销,这本质上是一种网络优化。只要这些操作是相互独立的,我们就可以使用这种技术。
Pipeline p = jedis.pipelined();
p.sadd("searched#" + userOneId, "paris");
p.zadd("ranking", 126, userOneId);
p.zadd("ranking", 325, userTwoId);
Response<Boolean> pipeExists = p.sismember("searched#" + userOneId, "paris");
Response<Set<String>> pipeRanking = p.zrange("ranking", 0, -1);
p.sync();
String exists = pipeExists.get();
Set<String> ranking = pipeRanking.get();
7.1.6、【发布/订阅】
我们可以使用Redis消息代理功能,在系统的不同组件之间发送消息。要确保订阅者和发布者线程不共享同一个Jedis连接。
7.1.6.1、【订阅者】
Jedis jSubscriber = new Jedis();
jSubscriber.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// 处理消息
}
}, "channel");
7.1.6.2、【发布者】
Jedis jPublisher = new Jedis();
jPublisher.publish("channel", "test message");
7.1.7、【连接池】
在实际的处理场景中,我们需要使用连接池来连接Redis。连接池是线程安全的。
final JedisPoolConfig poolConfig = buildPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost");
private JedisPoolConfig buildPoolConfig() {
final JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128);
poolConfig.setMaxIdle(128);
poolConfig.setMinIdle(16);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);
poolConfig.setMinEvictableIdleTimeMillis(Duration.ofSeconds(60).toMillis());
poolConfig.setTimeBetweenEvictionRunsMillis(Duration.ofSeconds(30).toMillis());
poolConfig.setNumTestsPerEvictionRun(3);
poolConfig.setBlockWhenExhausted(true);
return poolConfig;
}
由于连接池是线程安全的,所以可以将其静态存储在某个地方,但应该避免应用程序关闭时发生泄漏,所以要记着销毁连接池。
我们可以在需要的时候,从应用程序的任何地方使用它。
try (Jedis jedis = jedisPool.getResource()) {
// 用jedis资源进行操作
}
7.1.8、【Redis集群】
关于Redis集群的配置可以参考官方文档。
https://redis.io/topics/cluster-tutorial
我们只需要提供其中一个主实例的主机和端口信息,它就会自动发现集群中的其他实例。
try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) {
// 使用jedisCluster资源,跟一个普通的Jedis资源一样。
} catch (IOException e) {}
7.1.9、【Jedis官方网站】
https://github.com/xetorthio/jedis
7.2、【Redisson】
Redisson 是一个RedisJava 客户端,具有内存中数据网格的特点。
它是基于高性能异步处理的Java Redis客户端和Netty框架。
Java JDK兼容性是:1.8- 14, Android
7.2.1、【安装/配置】
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>x.x.x</version>
</dependency>
7.2.2、【列表】
// 缺省连接到 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
// 获取 java.util.List
RList<String> list = redisson.getList("myList");
list.add("1");
list.add("2");
list.add("3");
boolean contains = list.contains("1");
System.out.println("List size: " + list.size());
System.out.println("Is list contains value '1': " + contains);
for (String element : list) {
System.out.println("List element: " + element);
}
redisson.shutdown();
7.2.3、【映射】
// 缺省连接到 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
// 获取 java.util.concurrent.ConcurrentMap
RMap<String, Integer> map = redisson.getMap("myMap");
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
boolean contains = map.containsKey("a");
System.out.println("Map size: " + map.size());
System.out.println("Is map contains key 'a': " + contains);
Integer value = map.get("c");
System.out.println("Value mapped by key 'c': " + value);
boolean added = map.putIfAbsent("c", 4) == null;
System.out.println("Is value mapped by key 'c' added: " + added);
redisson.shutdown();
7.2.4、【锁】
// 缺省连接到 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
// 获取java.util.concurrent.locks.Lock
RLock lock = redisson.getLock("lock");
lock.lock();
System.out.println("lock aquired");
Thread t = new Thread() {
public void run() {
RLock lock1 = redisson.getLock("lock");
lock1.lock();
System.out.println("lock aquired by thread");
lock1.unlock();
System.out.println("lock released by thread");
};
};
t.start();
t.join(1000);
lock.unlock();
System.out.println("lock released");
t.join();
redisson.shutdown();
7.2.5、【原子操作】
// 缺省连接到 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
RAtomicLong atomicLong = redisson.getAtomicLong("myLong");
System.out.println("Init value: " + atomicLong.get());
atomicLong.incrementAndGet();
System.out.println("Current value: " + atomicLong.get());
atomicLong.addAndGet(10L);
System.out.println("Final value: " + atomicLong.get());
redisson.shutdown();
7.2.6、【Redisson官方网站】
https://github.com/redisson/redisson
8、【场景案例】
由于Redis数据库设计的特点,比较典型的用例有会话缓存、全页缓存、消息队列应用、排行榜和计数等,
- Twitter等大公司都在使用Redis,
- Amazon Web Services在其产品组合中提供了Redis服务,
- 微软在Azure中提供了Redis服务,
- 阿里巴巴在阿里云中提供了基于Redis的ApsaraDB。
9、【小结】
我们在本文中通过对Redis系统的设计理念,历史,与其他数据库的区别等方面分析和学习了Redis,希望对各位朋友有帮助。
欢迎讨论。
【参考】
https://en.wikipedia.org/wiki/Redis
- 点赞
- 收藏
- 关注作者
评论(0)