Redis
是开源,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串 strings,散列 hashes,列表 lists,集合 sets,有序集合 sorted sets 与范围查询, bitmaps,hyperloglogs 和 地理空间(geospatial)索引半径查询。
Redis 还内置了复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions)和不同级别的磁盘持久化(persistence),并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。
缓存 + 拆分
随着访问量的上升,一个数据库已经不能满足我们的需求了。为了更高的性能,我们在中间加上了一个缓存层并且将数据库做了集群、结构优化和读写分离。
缓存 拆分 分解图
这里的缓存就是 NoSQL,当然做缓存也只是 NoSQL 的一种功能,就像 Redis 并不仅仅有缓存这一种功能。比如它还能实现 简单的消息队列,解决Session共享,计数器,排行榜,好友关系处理 等等功能。
Redis 通用命令
keys
172.0.0.1:6379> set test1 hello
127.0.0.1:6379> set test2 world
127.0.0.1:6379> keys *test*
1) \"test2\"
2) \"test1\"
dbsize计算key的总数,这是redis内置的一个计算器,时间复杂度为O(1)。
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> dbsize
(integer) 4
127.0.0.1:6379> sadd myset a b c d e f g
(integer) 7
127.0.0.1:6379> dbsize
(integer) 5
exists检查key是否存在,存在返回1,不存在返回0
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> exists a
(integer) 1
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> exists a
(integer) 0
del删除指定key-value,可以同时删除多个,删除成功返回删除的key个数,如果不存在返回0,时间复杂度为O(1)。
127.0.0.1:6379> mset a b test1 hello test2 world
OK
127.0.0.1:6379> del a test1 test2
(integer) 3
127.0.0.1:6379> del a test1 test2
(integer) 0
expireexpire key seconds 设置key在seconds秒后过期,时间复杂度为O(1)。
127.0.0.1:6379> set test1 hello
OK
127.0.0.1:6379> expire test1 20
(integer) 1
ttlttl key 查看key剩余的过期时间,时间复杂度为O(1)。
127.0.0.1:6379> ttl test1
(integer) 13
127.0.0.1:6379> ttl test1
(integer) 8
127.0.0.1:6379> ttl test1
(integer) -2
# -2代表key已经不存在了
# 过期后再去查看key值,发现已经不存在了
127.0.0.1:6379> get test1
(nil)
persistpersist key 去掉key的过期时间,时间复杂度为O(1)。
127.0.0.1:6379> set test1 hello
OK
127.0.0.1:6379> expire test1 60
(integer) 1
127.0.0.1:6379> ttl test1
(integer) 56
# 去掉过期时间
127.0.0.1:6379> persist test1
(integer) 1
127.0.0.1:6379> ttl test1
(integer) -1
# -1代表key存在,但是没有过期时间
# 去掉过期时间后即便过期了也没有被删除
127.0.0.1:6379> get test1
\"hello\"
typetype key 返回key的类型,redis有5种数据结构,所以返回值有string、hash、list、set、zset,如果key不存在的,则返回none,时间复杂度为O(1)。
127.0.0.1:6379> type myset
none
127.0.0.1:6379> sadd myset a b c d e
(integer) 5
127.0.0.1:6379> type myset
set
Redis 的五种基本数据类型
Redis 作为缓存能实现的其他功能,比如计数器,排行榜,好友关系等,这些实现的依据就是靠着 Redis 的数据结构。在整个 Redis 中一共有五种基本的数据结构,他们分别是 字符串strings,散列 hashes,列表 lists,集合 sets,有序集合 sorted sets。
- String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
常用命令:set、get、decr、incr、mget等。
注意:一个键最大能存储512MB。
- Hash(哈希)
Redis hash 是一个键值(key=>value)对集合;是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
每个 hash 可以存储 232 -1 键值对(40多亿)。
常用命令:hget、hset、hgetall等。
应用场景:存储一些结构化的数据,比如用户的昵称、年龄、性别、积分等,存储一个用户信息对象数据。
- List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
list类型经常会被用于消息队列的服务,以完成多程序之间的消息交换。
常用命令:lpush、rpush、lpop、rpop、lrange等。
列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
- Set(集合)
Redis的Set是string类型的无序集合。和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
应用场景:
1、利用交集求共同好友。
2、利用唯一性,可以统计访问网站的所有独立IP。
3、好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。
常用命令:sadd、spop、smembers、sunion等。
集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
- zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
- sorted set是插入有序的,即自动排序。
常用命令:zadd、zrange、zrem、zcard等。
当你需要一个有序的并且不重复的集合列表时,那么可以选择sorted set数据结构。
应用举例:
(1)例如存储全班同学的成绩,其集合value可以是同学的学号,而score就可以是成绩。
(2)排行榜应用,根据得分列出topN的用户等。
redis存储文件格式
使用了两种文件格式:全量数据和增量请求。
全量数据格式是把内存中的数据写入磁盘,便于下次读取文件进行加载;
增量请求文件则是把内存中的数据序列化为操作请求,用于读取文件进行replay得到数据,序列化的操作包括SET、RPUSH、SADD、ZADD。
redis的存储分为内存存储、磁盘存储和log文件三部分,配置文件中有三个参数对其进行配置。
save seconds updates,save配置,指出在多长时间内,有多少次更新操作,就将数据同步到数据文件。这个可以多个条件配合,比如默认配置文件中的设置,就设置了三个条件。
appendonly yes/no ,appendonly配置,指出是否在每次更新操作后进行日志记录,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面的save条件来同步的,所以有的数据会在一段时间内只存在于内存中。
appendfsync no/always/everysec ,appendfsync配置,no表示等操作系统进行数据缓存同步到磁盘,always表示每次更新操作后手动调用fsync()将数据写到磁盘,everysec表示每秒同步一次。
Redis分布式锁
- 分布式锁在很多场景中是非常有用的原语, 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。
有很多分布式锁的库和描述怎么实现分布式锁管理器(DLM)的博客,但是每个库的实现方式都不太一样,很多库的实现方式为了简单降低了可靠性,而有的使用了稍微复杂的设计。
这个页面试图提供一个使用Redis实现分布式锁的规范算法。我们提出一种算法,叫Redlock,我们认为这种实现比普通的单实例实现更安全,请关注redis社区。
获取锁的时候,使用set加锁,使用expire命令为锁添加一个超时时间,锁的value值为一个随机生成的uuid。
获取锁的时候还设置一个获取超时的时间,若超过时间则放弃获取锁。
释放锁判断是否是此uuid 如果是则删除,释放锁。
优点:很多缓存服务都是集群部署避免单点问题,提供了很多实现分布式锁的方法。性能好实现起来方便。
缺点:通过超时时间来控制锁的失效时间不是十分靠谱。
Redis 大量数据插入
有些时候,Redis实例需要装载大量用户在短时间内产生的数据,数以百万计的keys需要被快速的创建。我们称之为大量数据插入(mass insertion)。
使用Luke协议,正常模式的Redis 客户端执行大量数据插入不是一个好主意:因为一个个的插入会有大量的时间浪费在每一个命令往返时间上。使用管道(pipelining)是一种可行的办法,但是在大量插入数据的同时又需要执行其他新命令时,这时读取数据的同时需要确保请可能快的的写入数据。
只有一小部分的客户端支持非阻塞输入/输出(non-blocking I/O),并且并不是所有客户端能以最大限度的提高吞吐量的高效的方式来分析答复。
分区:怎样将数据分布到多个redis实例
分区是将你的数据分发到不同redis实例上的一个过程,每个redis实例只是你所有key的一个子集。文档第一部分将介绍分区概念,第二部分介绍分区的另外一种可选方案。
Redis分区主要有两个目的:
- 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。
- 分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
分区基本概念
有许多分区标准。假如我们有4个Redis实例R0, R1, R2, R3,有一批用户数据user:1, user:2, … ,那么有很多存储方案可以选择。从另一方面说,有很多different systems to map方案可以决定用户映射到哪个Redis实例。
一种最简单的方法就是范围分区,就是将不同范围的对象映射到不同Redis实例。比如说,用户ID从0到10000的都被存储到R0,用户ID从10001到20000被存储到R1,依此类推。
这是一种可行方案并且很多人已经在使用。但是这种方案也有缺点,你需要建一张表存储数据到redis实例的映射关系。这张表需要非常谨慎地维护并且需要为每一类对象建立映射关系,所以redis范围分区通常并不像你想象的那样运行,比另外一种分区方案效率要低很多。
另一种可选的范围分区方案是散列分区,这种方案要求更低,不需要key必须是object_name:<id>的形式,如此简单:
- 使用散列函数 (如 crc32 )将键名称转换为一个数字。例:键foobar, 使用crc32(foobar)函数将产生散列值93024922。
- 对转换后的散列值进行取模,以产生一个0到3的数字,以便可以使这个key映射到4个Redis实例当中的一个。93024922 % 4 等于 2, 所以 foobar 会被存储到第2个Redis实例。 R2 注意: 对一个数字进行取模,在大多数编程语言中是使用运算符%
还有很多分区方法,上面只是给出了两个简单示例。有一种比较高级的散列分区方法叫一致性哈希,并且有一些客户端和代理(proxies)已经实现。
不同的分区实现方案
分区可以在程序的不同层次实现。
- 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
- 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
- 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。
订阅发布 Pub/Sub
订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者)。而是发布的消息分到不同的频道,不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的。这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。
为了订阅foo和bar,客户端发出一个订阅的频道名称:
SUBSCRIBE foo bar
其他客户端发到这些频道的消息将会被推送到所有订阅的客户端。
客户端订阅到一个或多个频道不必发出命令,尽管他能订阅和取消订阅其他频道。订阅和取消订阅的响应被封装在发送的消息中,以便客户端只需要读一个连续的消息流,其中第一个元素表示消息类型。
推送消息的格式-消息是一个有三个元素的多块响应 。
第一个元素是消息类型:
- subscribe: 表示我们成功订阅到响应的第二个元素提供的频道。第三个参数代表我们现在订阅的频道的数量。
- unsubscribe:表示我们成功取消订阅到响应的第二个元素提供的频道。第三个参数代表我们目前订阅的频道的数量。当最后一个参数是0的时候,我们不再订阅到任何频道。当我们在Pub/Sub以外状态,客户端可以发出任何redis命令。
- message: 这是另外一个客户端发出的发布命令的结果。第二个元素是来源频道的名称,第三个参数是实际消息的内容。
数据库与作用域
发布/订阅与key所在空间没有关系,它不会受任何级别的干扰,包括不同数据库编码。 发布在db 10,订阅可以在db 1。 如果你需要区分某些频道,可以通过在频道名称前面加上所在环境的名称(例如:测试环境,演示环境,线上环境等)。
模式匹配订阅
Redis 的Pub/Sub实现支持模式匹配。客户端可以订阅全风格的模式以便接收所有来自能匹配到给定模式的频道的消息。
redis架构
redis使用场景
1 会话缓存
最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。
2 全页缓存
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,类似PHP本地FPC。
3 队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(Python)对 list 的 push/pop 操作。
4 排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。
集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。
5 发布/订阅
Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。
常考-Redis集群
哨兵模式Sentinel,它是为了解决Redis集群的高可用应运而生的。Redis集群,一般来说只会有一个master服务,当master挂掉之后,之能手动切换master就会造成一段时间内的服务不可用。sentinel就是专门针对这种情况而产生的一个监听服务。它主要负责监听我们一个组内所有Redis,当我们的master挂掉之后,它就会根据Raft算法进行选举一台新的leader出来,然后将选举出来的leader当成我们的master。
Redis使用实例
要想在Java中连接Redis,并进行操作,由两种方式,一种是spring data redis,它是由spring集成的,不支持集群,一种是官方推荐的jedis,支持集群。
- 下载jedis-2.7.3.jar
- 创建redis.properties配置文件:
redis.host=127.0.0.1
redis.port=6379
redis.password=
redis.timeout=100000
redis.maxIdle=100
redis.maxActive=300
redis.maxWait=1000
redis.testOnBorrow=true
- Redis工具类:
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import redis.clients.jedis.Jedis;
/**
* 操作redis缓存的工具类,简单的demo,方便入门初步理解redis
*
* @author Xuan
*
*/
public final class RedisUtil {
public RedisUtil() {
}
// 静态代码块
static {
// 加载properties配置文件
Properties properties = new Properties();
InputStream is = RedisUtil.class.getClassLoader().getResourceAsStream(
\"redis.properties\");
try {
properties.load(is);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
host = properties.getProperty(\"redis.host\");
port = properties.getProperty(\"redis.port\");
password = properties.getProperty(\"redis.password\");
timeout = properties.getProperty(\"redis.timeout\");
maxIdle = properties.getProperty(\"redis.maxIdle\");
maxActive = properties.getProperty(\"redis.maxActive\");
maxWait = properties.getProperty(\"redis.maxWait\");
testOnBorrow = properties.getProperty(\"redis.testOnBorrow\");
// 得到Jedis实例并且设置配置
jedisxuan = new Jedis(host, Integer.parseInt(port),
Integer.parseInt(timeout));
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public static boolean set(final String key, String value) {
boolean result = false;
try {
jedisxuan.set(key, value);
result = true;
} catch (Exception e) {
System.out.println(\"set cache error\");
}
return result;
}
/**
* 读取缓存
*
* @param key
* @return
*/
public static Object get(final String key) {
Object result = null;
result = jedisxuan.get(key);
return result;
}
/**
* 删除key对应的value
*
* @param key
*/
public static void remove(final String key) {
if (key != null && key.length() >= 1 && !key.equals(\"\")
&& jedisxuan.exists(key)) {
jedisxuan.del(key);
}
}
/**
* 判断缓存中是否有key对应的value
*
* @param key
* @return
*/
public static boolean exists(final String key) {
return jedisxuan.exists(key);
}
/**
* 写入缓存(规定缓存时间)
*
* @param key
* @param value
* @param expireSecond
* @return
*/
public static boolean set(final String key, String value, Long expireSecond) {
boolean result = false;
try {
// NX代表不存在才set,EX代表秒,NX代表毫秒
jedisxuan.set(key, value, \"NX\", \"EX\", expireSecond);
result = true;
} catch (Exception e) {
System.out.println(\"set cache error\");
}
return result;
}
}
- 使用主方法
/**
* 测试类
* @author Xuan
*
*/
public class test {
/**
* @param args
*/
public static void main(String[] args) {
// 写入一个缓存
boolean flag = RedisUtil.set(\"x\", \"007\");
if (flag) {
// 读取缓存
System.out.println(\"成功写入缓存\");
System.out.println(\"正在读取缓存......\");
String leo = String.valueOf(RedisUtil.get(\"x\"));
System.out.println(\"你读取的缓存为:\" + leo);
} else {
System.out.println(\"写入缓存失败\");
}
//写入一个带时间的缓存 30秒消失
//可以自己去验证是否正确
boolean flag1 = RedisUtil.set(\"LEO\", \"时间缓存测试~\",Long.parseLong(\"30\"));
if (flag){
System.out.println(\"写入成功\");
}
}
}