Redis入门

NoSQL数据库简介

web1.0时代,数据访问量很有限,当一夫当关的高性能的单点服务器可以解决大部分问题。

web2.0时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临着巨大的性能挑战。(服务器CPU和内存的压力,数据库服务的IO压力)

解决CPU和内存压力

采用负载均衡,集群和分布式架构。

session问题?

方案一:存储到客户端cookie,缺点:安全性很难保证,网络负担效率低

方案二:session复制。复制session到多台服务器中。缺点:session数据冗余,节点越多,浪费越大

方案三:使用nosql数据库。完全在内存中,速度快,数据结构简单。

解决数据库服务的IO压力

使用NoSQL数据库作为缓存数据库,减少io的读操作

NoSQL数据库

NoSQL数据库概述

Not Only SQL,非关系型的数据库。NoSQL不依赖业务逻辑方式存储,而是以简单的Key-Value模式存储。因此大大的增加了数据库的扩展能力。

  • 不遵循SQL标准
  • 不支持ACID
  • 远超于SQL的性能

NoSQL适用场景

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性

NoSQL不适用场景

  • 需要事务支持
  • 基于SQL的结构化查询存储,处理复杂的关系,需要即席查询

常见的NoSQL数据库

  • Memcache
  • Redis
  • MongoDB

行式数据库

列式数据库

Hbase

图关系型数据库

Redis概述安装

配合关系型数据库做高速缓存、多样的数据结构存储持久化数据

安装

只考虑在Linux环境中。

准备工作:下载安装最新版的GCC编译器

安装C语言的编译环境

1
2
3
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash

测试GCC版本

1
gcc --version

将redis.tar.gz放入/opt目录中

解压

1
tar -zxvf redis.tar.gz

进入解压完的目录

1
cd redis

在redis目录下再次执行make命令

执行make install

安装目录为/user/local/bin

查看默认安装目录

redis-benchmark:性能测试工具
redis-check-aof:修复有问题的AOF文件
redis-check-dump:修复有问题的dump.rdb文件
redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令
redis-cli:客户端,操作入口

前台启动

1
redis-server

后台启动

拷贝一份redis.conf到其它目录

1
cp /opt/redis-3.2.5/redis.conf /myredis

后台启动设置daemonize no改成yes:修改redis.conf文件,将里面的daemonize no改成yes,让服务器在后台启动

redis启动

1
redis-server/myredis/redis.conf

用客户端访问:redis-cli

Redis关闭

单实例关闭:redis-cli shutdown

也可以进入终端后再关闭

多实例关闭,指定端口关闭:redis-cli - p 6379 shutdown

Redis相关知识介绍

端口号6379从何而来

默认16个数据库,类似数组下标从0开始,初始默认使用0号库

使用命令select dbid来切换数据库。例如:select 8

统一密码管理,所有库同样密码。

dbsize 查看当前数据库的key的数量

flushdb清空当前库

fiushall通杀全部库

Redis单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符Socket的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行正真的操作可以在同一个线程里执行,也可以启动线程执行

Redis-key键操作

查看当前库所有的key

1
keys *

判断某个key是否存在,返回1表示存在;返回0表示不存在

1
exists key名称

查看你的key是什么类型

1
type key名称

删除指定的key数据,unlink是选择非阻塞删除,将keys从keyspace元数据中删除,正真的删除会在后续异步操作

1
2
del key名称
unlink key名称

为给定的key设置过期时间

1
expire key名称 10

查看key还有多少秒过期,-1表示永不过期,-2表示已过期

1
ttl key名称

使用命令select dbid来切换数据库。例如:select 8

统一密码管理,所有库同样密码。

dbsize 查看当前数据库的key的数量

flushdb清空当前库

fiushall通杀全部库

Redis字符串String

简介

String类型是二进制安全的,意味着Redis的String可以包含任何数据,例如jpg图片或者序列化的对象

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

常用命令

添加键值对

1
set <key> <value>

查询对应键值

1
get <key>

将给定的\追加到原值的末尾,并返回总共的长度

1
append <key> <value>

获取值的长度

1
strlen <key>

只有在key不存在时,设置key的值

1
setnx <key> <value>

使得储存的数字值增1,只能对数字值进行操作,如果为空,新增值为1

1
incr <key>

使得储存的数字值减1,只能对数字值进行操作,如果为空,新增值为-1

1
decr <key>

将key中存储的数字值增减。自定义步长

1
incrby/decrby <key> <step>

原子性

原子操作是指不会被线程调度机制打断操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch上下文切换。在单线程中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只能发生于指令之间;在多线程中,不能被其它线程打断的操作就叫原子操作。Redis单命令的原子性主要得益于Redis的单线程。

同时设置一个或多个key-value对

1
mset <key1> <value1> <key2> <value2> ......

同时获取一个或多个value

1
mget <key1> <key2> <key3>

同时设置一个或多个key-value对,当且仅当所有key都不存在时设置成功

1
msetnx <key1> <value1> <key2> <value2> ......

获取值的范围,类似于java中的substring,前包含,后包含

1
getrange <key> <起始位置> <结束位置>

覆写\所存储的字符串值,从\<起始位置>开始

1
setrange <key> <起始位置> <value>

设置键值的同时设置过期时间,单位秒

1
setex <key> <过期时间> <value>

设置新值获取旧值

1
getset <key> <value>

数据结构

String的数据结构为简单的动态字符串(Simple Dynamic String, SDS)。是可以修改的字符串,内部结构类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

加倍扩容。在需要空间达1M之前按新空间两倍分配空间,否则按新空间大小+1M分配。注意,1M=10241024\Char。Char可以是5bits/8bits/16bits/32bits/64bits需要注意的是字符串最大长度为512M。

SDS定义

1
2
3
4
5
6
7
8
9
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}

实现的好处:

  • 常数复杂度获取字符串长度;
  • 杜绝缓冲区溢出:对于SDS数据类型,在进行字符修改的时候,会首先根据记录的len属性检查内存空间是否满足需求,如果不满足会进行相应的空间扩展,然后再进行修改操作,所以不会出现缓冲区溢出;
  • 减少修改字符串的内存重新分配次数:对于SDS,由于len属性和free属性的存在对于修改字符串SDS实现了空间预分配惰性空间释放两种策略:
    • 空间预分配:对字符串进行空间扩展时,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需要的内存重新分配次数。
    • 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用free属性将这些字节的数量记录下来,等待后续使用。
  • 二进制安全:C字符串以空字符作为字符串结束的标识,而SDS不是以空字符串来判断是否结束,而是以len属性表示的长度来判断字符串是否结束;
  • 兼容部分C字符串函数:虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库 中的一部分函数。

Redis列表List

简介

单键多值

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部和尾部。

它的底层实际上是双向链表,对两端的操作性很高,对于通过索引下标的操作中间的节点性能会较差。

常用命令

从左边/右边插入一个或多个值。

1
lpush/rpush <key> <value1> <value2> <value3> ......

从左边/右边吐出一个值。值在键在,值光键亡

1
lpop/rpop <key>

从\列表右边吐出一个值,插到\列表的左边

1
rpoplpush <key1> <key2>

按照索引下标获得元素(从左到右)

1
2
lrange <key> <start> <end>
lrange mylist 0 -1 全部都能取到

按照索引位置获得元素(从左到右)

1
lindex <key> <index>

获取列表长度

1
llen <key>

在\的后面插入\插入值

1
linsert <key> before <value> <newvalue>

从左边删除n个value

1
lrem <key> <n> <value>

将列表key下标为index的值替换成value

1
lset <key> <index> <value>

数据结构

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的是int类型的数据,结构上还需要两个额外的指针prev和next。

redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双指针串起来使用。这样既能满足快速的插入删除性能,又不会出现太大的空间冗余。

1
2
3
4
5
6
7
8
typedef  struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsignedlong len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;

Redis链表特性:

  • 双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度为O(1)
  • 无环:表头结点的prev和表尾结点的next指针指向NULL
  • 带链表长度计数器:通过len属性获取链表长度的时间复杂度为O(1)
  • 多态:链表结点使用void*指针来保存结点值,可以保存各种不同类型的值。

Redis集合(Set)

简介

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的set是String类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

常用命令

将一个或多个member元素加入到集合key中,已经存在的member元素将被忽略

1
sadd <key> <value1> <value2>......

取出该集合的所有值

1
smembers <key>

判断集合\是否含有\值,有1,没有0

1
sismember <key> <value>

返回集合中元素的个数

1
scard <key>

删除集合中的某个元素

1
srem <key> <value1> <value2> ......

随机从集合中吐出一个值

1
spop <key>

随机从集合中取出n个值,但不会从集合中删除

1
srandmember <key> <n>

把集合中的一个值从一个集合中移到另一个集合中

1
smove <source> <destination> <value>

返回两个集合的交集元素

1
sinter<key1> <key2>

返回两个集合的并集元素

1
sunion<key1> <key2>

返回两个集合的差集元素(key1中包含的,但key2中不包含的)

1
sdiff<key1> <key2>

数据结构

Set数据结构是dict字典,字典是用哈希表实现的。Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis中的set结构也是一样,它的所有的value都指向同一个内部值。

Redis哈希(Hash)

简介

Redis hash 是一个键值对集合。

Redis hash是一个String类型的field和value的映射表,hash特别适合用于存储对象。

类似Java中的Map\

常用命令

给\集合中的\键赋值\

1
hset<key> <field> <value>

从\集合中的\取出值\

1
hget <key> <field>

批量设置hash值

1
hmset<key> <field1> <value1> <field2> <value2>......

查看哈希表key中,给定字段\是否存在,存在为1,不存在为0

1
hexists <key1> <field>

查看hash集合中的所有的field

1
hkeys <key>

查看hash集合中的所有的value

1
hvals <key>

为哈希表key中的field的值加上增量1 -1

1
hincrby <key> <field> <increment>

将哈希表key中field的值设置为value,当且仅当field不存在

1
hsetnx <key> <field> <value>

数据结构

Hash类型对应的数据结构有两种:ziplist压缩列表、hashtable哈希表。当字段长度较短且个数较少时,使用ziplist;否则使用hashtable。

字典:

1
2
3
4
5
6
7
8
9
10
11
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

哈希表结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;

}dictht

dictEntry结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;

//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry

key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突

  • 哈希算法:
    • 使用字典设置的哈希函数,计算键key的哈希值hash = dict -> type -> hashFunction(key)
    • 使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值index = hash & dict ->ht[x].sizemask
  • 解决哈希冲突:链地址法
  • 扩容和收缩:
    • 如果执行扩展操作,会基于原哈希表创建一个等于ht[0].used * 2n的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩的是根据已使用空间缩小一倍创建一个新的哈希表;
    • 重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上;
    • 所有键值对都迁徙完毕后,释放原来的哈希表的内存空间。
  • 触发扩容的条件:
    • 服务器目前没有执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于1。
    • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于5。
    • 负载因子=哈希表已保存的节点数量/哈希表的大小
  • 渐进式rehash:
    • 扩容和收缩操作不是一次性、集中式完成的,而是分多次,渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么rehash可以瞬间完成,但如果键值对有几百万,几千万甚至几亿,那么要一次性的进行rehash,势必会造成redis一段时间内不能进行别的操作。所以Redis采用渐进式rehash,这样在渐进式rehash期间,字典的删除查找更新等操作会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是增加操作,一定在新的哈希表上进行。

Redis有序集合(Zset)

简介

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但评分可以是重复的。

因为元素是有序的,所以你也可以很快根据评分或者次序来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的列表。

常用命令

将一个或多个member元素及其score值加入到有序集合key当中

1
zadd <key> <score1> <value1> <socre2> <value2>......

返回有序集合key中,下标在\\之间的元素,带withscores,可以让分数一起和值返回到结果集中。

1
zrange <key> <start> <stop> [WITHSCORES]

返回有序集合key中,所有score在\\之间的元素,有序集成员按score值递增。

1
2
3
4
zrangebyscore <key> <min> <max> [WITHSCORES] [limit offset count]

zrangebyscore <key> <max> <min> [WITHSCORES] [limit offset count]
从大到小

为元素的score加上增量

1
zincrby <key> <increment> <value>

删除该集合下,指定值元素

1
zrem <key> <value>

统计该集合,分数区间内的元素个数

1
zcount<key> <min> <max>

返回该值在集合中的排名,从0开始

1
zrank <key> <value>

数据结构

Zet是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map\,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

Zset底层使用了两个数据结构:

hash,hash的作用是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score的值。

跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

redisDb

1
2
3
4
5
6
7
8
9
10
11
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
1
2
3
4
5
6
7
8
9
10
11
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小 4
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;

}dictht
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;

//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry

Redis配置文件详解

Units 单位

配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,大小写不敏感

Include

多实例的情况可以把公用的配置文件提取出来。

网络相关配置

bind

默认情况下bind=127.0.0.1只能接受本机的访问请求;不写的情况下,无限制接收任何ip地址的访问;生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉。

如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应

protected-mode

将本机访问保护模式设置no

Port

端口号,默认为6379。

tcp-backlog

设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列+已经完成三次握手队列。

在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。

Redis的发布和订阅

什么是发布和订阅

Redis的发布和订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。

Redis客户端可以订阅任意数量的频道。

客户端可以订阅频道;当给这个频道发布消息后,消息就会发送给订阅的客户端。

发布订阅命令行实现

打开一个客户端订阅channel1

1
SUBSCRIBE channel1

打开另一个客户端,给channel1发布消息hello

1
publish channel1 hello

Redis新型数据类型

Bitmaps

合理使用操作位能够有效地提高内存使用率和开发效率

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

  • Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操作
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组下标在Bitmaps中叫作偏移量。

命令

setbit

设置Bitmaps中某个偏移量的值(0或1);offset偏移量从0开始

1
setbit <key> <offset> <value>

实例:每个独立的用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户id。设置键的第offset个位的值,假设现在有20个用户,userid=1,6,11,15,19的用户对网站进行了访问。

1
2
3
4
5
setbit users:20210101 1 1
setbit users:20210101 6 1
setbit users:20210101 11 1
setbit users:20210101 15 1
setbit users:20210101 19 1

getbit

获取Bitmaps中某个偏移量的值

1
getbit <key> <offset>

bitcount

统计字符串被设置为1的bit数,一般情况下,给定的整个字符串都会被进行计数,通过指定额外的start或end参数,可以让计数只在特定的位上进行。start和end参数的设置,都可以使用负数值:比如-1表示最后一位,而-2表示倒数第二个位,start、end是指bit组的字节的下标数,二者皆可包含。

1
bitcount <key> [start end]

bitop

bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。

1
bitop and(or/not/xor) <destkey> [key...]
Donate comment here