Redis是完全开源免费的,遵守BSD新协议,是一个高性能(MySQL)的K-V数据库,Redis是一个开源的使用ANSI,C语言编写,支持网络,可基于内存亦可持久化日志型、K-V数据库,并提供多种语言API,从2010年3月15日起,Redis的开发工作由VMware主持,从2013年5月开始Redis的开发由Pivotal赞助。
BSD:
开源协议是一个给予使用者很大自由的协议,可以自由的使用,修改代码,也可以将修改后的代码做为开源或专有软件再发布,BSD代码鼓励代码共享,但需要尊重代码作者著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此对商业集成很友好的协议。
键值(K-V)
列存储数据库
文档数据库
图形数据库
最终:
1.数据模型比较简单
2.需要灵活性更强的IT系统
3.对数据库性能要求较高
4.不需要高度的数据一致性
5.对于给定key,比较容易映射复杂的环境
Redis单个操作都是原子性,意思是要么成功执行要么失败完全不执行。单个操作是原子性的多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
小结:
Redis单个key存入512M大小
支持多种类型数据结构(string,list,hash,set,zset)
单线程 原子性
可以持久化 使用RDB和AOF
支持集群 而且redis支持库(0-15)
可以做消息队列,比如IM,聊天室
# 企业开发中:可以存储热点数据和消息中间价等大部分功能
windows:
Linux:官方下载
# 环境依赖安装gcc
yum -y install gcc automake autoconf libtool make
# 如果安装出现/var/run/yum.pid 已被锁定PID为xxx的另一个进程正在运行问题:
rm -f /var/run/yum.pid
# 安装redis
wget http://download.redis.io/releases/redis-4.0.1.tar.gz
tar zxvf redis-4.0.1.tar.gz
cd redis-4.0.1
make 或make MALLOC=libc
# 安装编译后的文件,安装到指定目录
make PREFIX=/usr/local/redis install
启动服务:
# 启动erdis服务端
./redis-server
# 启动redis客户端:redis -h IP地址 -p 端口
./redis-cli
检测redis是否启动
#客户端执行:
127.0.0.1:6379> ping
PONG
# 返回PONG表示启动成功
Redis配置文件位于Redis安装目录下,文件名为redis.conf(windows名为redis.windows.conf)
cp redis.conf /usr/local/redis
查看redis.conf
less -mN redis.conf
redis相关配置参数
# 表示不是以守护进程访问的,当为yes,可以后台启动
daemonize no
# 表示redis 的pid写入文件地址。
pidfile /var/run/redis_6379.pid
# 配置但口号
port 6379
# 绑定IP地址,当前只能本机只能访问到。当注释掉,其他机器都能访问
bind 127.0.0.1
# 当前客户端闲置多长时间关闭连接,如果为0,表示关闭该功能
timeout 300
# 指定日志级别,Redis总共支持四个级别:debug,verbose,notice,warning 默认verbose
logfile stdout
# 设置数据库数量,默认数据库为0,可以使用select <db> 命令在连接上指定数据库id
databases 16
# 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
save 900 1
save 200 10
save 60 10000
# 上面参数意思:分别表示900秒内有一个更改,300秒内有10个更改以及60秒内有10000个更改。三个条件满足一个就做一次持久化数据同步
# 指定存储至本地数据库是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变得特别大
rdbcompression yes
# 指定本地数据库文件名,默认为dump.rdb
dbfilename dump.rdb
# 指定本地数据库存放目录
dir ./
#设置当本机为slave服务时,master服务的IP地址及端口,在redis启动时,它会自动从master进行数据同步
slaveof <masterip> <masterport>
#当master服务设置了密码保护时,slave服务连接master的密码
masterauth <master-password>
# 设置Redis连接密码,如果配置了连接密码,客户端在连接redis时需要 AUTH <password>命令提供密码,默认关闭
requirepass foobared
# 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开客户端连接数为redis进程可以打开的最大文件描述符数,如果设置maxclients 0,表示不作限制。当客户端连接到大限制时,Redis会关闭新的连接并向客户端返回max_number of clients reached 错误信息
maxclients 128
# 指定redis最大内存限制。redis在启动时会把数据加载到内存中,达到最大内存后,redis会尝试清除已到期或即将到期的key,当此方法处理后,仍然到大最大内存设置。将无法再进行写入操作,但仍然可以进行读取操作,redis新的vm机制,会把key存放内存,value存放在swap区中(建议:1.为数据设置超时时间,设定内存空间建议不超过256-512M(当有1G空间时候)。2.采用LRU算法动态将不用的数据删除)
maxmemory <bytes>
# LRU算法配置:
内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU。操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
1.volatile-lru:设定超时时间的数据中,删除最不常用数据
2.allkeys-lru:查询所有key中最近不常使用的数据进行删除,这是应用最广泛的策略
3.volatile-random:在已经设定了超时的数据中随机删除
4.allkeys-random:查询所有key之后随机删除
5.volatile-ttl:查询全部设定超时时间的数据,之后排序,将马上将要过期的数据进行删除操作
6.Noevication:如果设置为该属性,则不会进行删除操作,如果内存溢出则报错返回
7.volatile-lfu:从所有配置过期时间的键中驱逐使用频率最少的键
8.allkeys-lfu:从所有键中驱逐使用频率最少的键
# 服务端启动:
./bin/redis-server ./redis.conf
# 启动客户端:
./bin.redis-cli -h <ip> -p <port> -a <password>
# 本机启动redis: ./bin.redis-cli -a 123
127.0.0.1:6379> set gradeName Python
OK
127.0.0.1:6379> keys *
1) "gradeName"
127.0.0.1:6379> keys gradeName
1) "gradeName"
127.0.0.1:6379> get gradeName
"Python"
ps -ef | grep -i redis
kill -9 REDIS_PID
./bin/redis-cli shutdown
e.g.:
# 关闭redis server端
127.0.0.1:6379> shutdown
not connected>
# 查询redis,可以看到redis的server端没了
ps -ef | grep redis
root 8252 7087 0 22:01 pts/0 00:00:00 ./bin/redis-cli -a xujunkai
root 8255 7986 0 22:07 pts/1 00:00:00 grep --color=auto redis
# 如果此时启动redis 服务端和客户端,查询数据还是存在的
del [key]
127.0.0.1:6379> del gradeName
(integer) 1
# 返回1表示删除成功,返回0表示删除失败
keys *
# 看所有key
keys *
dump [key]
# 返回序列化的值
dump a
"\x00\xc0{\b\x00\xf1\xcf\xbf^\xa9c\xed\x81"
exists [key]
# 判断key是否存在
127.0.0.1:6379> exists a
(integer) 1
# 返回1表示存在,返回0表示不存在
ttl [key]
# 查看key 的剩余生存时间,以秒为单位
127.0.0.1:6379> ttl a
(integer) -1
# -1 表示永久
expire [key] [seconds]
# 给key为b 设置过期时间10秒
127.0.0.1:6379> expire b 10
(integer) 1
# 当返回-2表示,无效
127.0.0.1:6379> ttl b
(integer) -2
用途:
1.限时优惠活动信息
2.网站数据缓存(对于一些需要定时更新的数据,如积分排行)
3.手机验证码
4.限制网站访问频率(1分钟访问10次)
pexpire [key] [seconds]
# 为给定key设置过期时间(以毫秒)
pttl [key]
# 查看key 的剩余生存时间,以毫秒为单位
persist key
# 移除key的过期时间,永久有效
以通配符方式查看key
keys user?
keys *
* 代表所有
? 代表一个字符
random [key]
# 从当前数据库中随机返回一个key
rename [key] [newkey]
# 重新命名key
127.0.0.1:6379> rename a aaa
OK
move [key] [db]
# 将当前数据库的key移动到指定库内
# 将key为a 移动到 db=1的库
127.0.0.1:6379> move aaa 1
(integer) 1
type [key]
# 查看储存值的类型
127.0.0.1:6379> type b
string
key命名建议:
1.key不要太长,尽量不要超过1024字节,这不仅消耗内存,而且会降低查找频率。
2.key不要太短,太短key的可读性降低
3.在一个项目中key最好使用统一命名模式,例如:user:123:password
4.key名称区分大小写
# 1.赋值语法
# redis set命令用于设置给定key的值,如果key已经存储值,set会将旧值覆盖,且无视类型。
set key_name value [ex秒数] [px毫秒] [nx/xx]
# 只有在key不存在时设置key的值。命令在指定key不存在时,为key设置指定的值
setnx key value
# 同时设置一个或多个key-value对
mset key value
# 2.取值语法
# redis get命令用于获取指定key的值,如果key不存在,返回nil.如果key储存的值不是字符串类型,返回一个错误。
get key_name
# 用于获取存储在指定key中字符串的子字符串,字符串的截取范围由start和end两个偏移量决定(包括start和end在内)
getrange key start end
e.g.:
127.0.0.1:6379> getrange gradeName 0 4
"go110"
# 对key 所储存的字符串值,获取指定偏移量上的位(bit)
getbit key offset
# 获取所有(一个或多个)给定key的值
MGET key1 [key2,...]
# getset命令用于设置指定key的值,并返回key的旧值,当key不存在时,返回nil
GETSET key_name value
e.g.:
# gradeName存在。grade不存在
127.0.0.1:6379> getset gradeName python1211
# 返回旧值
"go1101"
# grade不存在,返回nil,并创建 key为grade ,value为python
127.0.0.1:6379> getset grade python
(nil)
# 返回key所储存的字符串值的长度
strlen key
# 3.删除语法:
# 删除指定key,如果存在,返回数字类型
del key_name
# 4.自增or自减
# Incr命令将key中储存的数字值增1,如果key不存在,那么key的值会先被初始化为0,然后再执行incr 操作
incr key
e.g.:
incr topic:num
(integer) 1
# 指定自增值
incrby key [num:int]
e.g.:
incrby topic:num 10
# 自减 同上原理,如果key不存在,什么都不做返回-1
decr key_name
# 自减 指定自减值
decrby key [num:int]
# 字符串拼接,为指定key追加至末尾,如果不存在,为其赋值
# append 命令用于为指定key追加至末尾,如果不存在,为其赋值
append key_name new_value
字符串的结构:
类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象。 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象。 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象。 |
字符串编码解析:
1.如果一个字符串对象保存是整数,那么字符串对象会将整数值保存在字符串对象结构中,并将字符串对象编码设置int。
2.如果字符串对象保存字符串,并且长度大于39字节。那么字符串对象将使用一个动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。(二进制)
3.如果字符串对象保存字符串,并且长度小于等于39字节,那么字符串对象将将使用embstr编码来保存字符串
# 可以这么认为embstr用于保存短的字符串,它和raw编码一样。都是通过redisObject和sdshdr结构来表示字符串对象。需要值的注意的是,embstr编码通过调用一次内存分配函数来分配一块连续空间。空间中依次包含RedisObject和sdshdr两个结构。
# 采用int和embstr编码原因是相对于raw格式,raw需要分配2次内存。而这2中只需要调用一次内存空间,然后释放内存。
应用场景:
1.String通常用于保存单个字符串或json字符串数据
2.因为String是二进制安全,所以完全可以把一个图片文件的内容做为字符串来存储
3.计数 常规-key-value 缓存应用 常规计数:微博数,粉丝数
4.INCR等指令本身就具有原子操作的特性,所以我们完全可以利用redis的INCR,INCRBY,DECR,DECRBY等指令来实现原子计数的效果。假如在某种场景下有3个客户端同时读取了mynum的值,然后其同时进行了加1的操作,那么最后mynum的值一定是5。不少网站都利用redis的这个特性来实现业务上的统计计数需求。
# 1.赋值语法:
# 为指定的key,设定field/value
hset key field value
e.g.:
hset h1 username lilei
# 同时将多个field-value(域-值),设置到哈希表key中
e.g.:
hmset users uname guo age 20 address beijing
# 2.取值语法
# 获取存储在hash中的值,根据field得到value
hget key field
e.g.:
127.0.0.1:6379[1]> hget users address
"beijing"
# 获取key所有给定字段的值
hmget key field
e.g.:
127.0.0.1:6379[1]> hmget users uname age address
1) "lilei"
2) "200"
3) "beijing"
# 返回hash表中所有字段和值
hgetall key
e.g.:
127.0.0.1:6379[1]> hgetall users
1) "uname"
2) "lilei"
3) "age"
4) "200"
5) "address"
6) "beijing"
# 获取key所有给定的字段
hgetall key
e.g.:
127.0.0.1:6379[1]> hkeys users
1) "uname"
2) "age"
3) "address"
# 获取哈希表中字段的数量
hlen key
e.g.:
127.0.0.1:6379[1]> hlen users
(integer) 3
# 3.删除语法
# 删除一个或多个hash表字段
hdel key field1 [field2]...
e.g.:
# 删除users对象 uname和age
127.0.0.1:6379[1]> hdel users uname age
(integer) 2
# 4.其他语法
# 只有在字段不存在时,设置哈希表字段值
hsetnx key field value
# 为哈希表key中指定字段的整数值加上增量increment
hincrby key field increment
# 为哈希表key中指定字段的浮点数值加上增量 increment
hincrbyfloat key field increment
# 查看哈希表key中,指定字段是否存在
hexists key field
1.用于存储一个对象
# hash是最贴近关系型数据库数据类型,可以将数据库一条记录或程序中一个对象转换成hashmap存放在redis中。用户ID未查找的key,存储的value用户对象如姓名,年龄等信息,如果用普通key-value结构来存储:
一般存储方式:
1.用户id做为查找key,把其他信息封装成一个对象以序列化方式存储,这种方式缺点是,增加序列化/反序列化开销,并且需要修改其中一项时,需要把占整个对象取回,并且修改操作需要对并发进行保护,引入cas等复杂问题。
2.这个用户对象有多少成员就存在多少key-value对,用户ID-->对应属性名称做为唯一标识来取得对应属性的值。虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费开始非常多的、
而redis的hash很好解决这个问题,redis的hash实际是内部存储value为一个hashmap,并提供直接存取这个map成员接口
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
哈希对象保存的键值对数量小于512个
hash-max-ziplist-value 64 # ziplist中最大能存放的值长度
hash-max-ziplist-entries 512 # ziplist中最多能存放的entry节点数量
对于使用 ziplist 编码的列表对象来说, 当使用 ziplist 编码所需的两个条件的任意一个不能被满足时, 对象的编码转换操作就会被执行: 原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面, 对象的编码也会从 ziplist 变为 hashtable 。并且这个转换过程也是一个不可逆的过程。
哈希对象结构如下:
typedef struct redisObject {
unsigned type:4; // hash类型
unsigned encoding:4; // //对象的编码类型,分别为 OBJ_ENCODING_ZIPLIST 或 OBJ_ENCODING_HT
unsigned lru:LRU_BITS; // 上一次操作的时间
int refcount; // 引用计数,便于内存管理
void *ptr; // 指向底层的数据结构
} robj;
当哈希对象底层采用ziplist实现时候,每次插入一个新的键值对,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,类似如下:
zibytes | zitail | zllen | entry1 | entry2 | entry3 | entry4 | zlend | key1 | value1 | key2 | value2
当哈希对象底层采用hashtable实现的时候,哈希对象中的指针ptr会指向一个dict,哈希对象中的每个键值对都使用一个字典键值对来保存, 每个键和值都是一个字符串对象。
hashtable的实现:
命令 | ziplist编码实现方法 | hashtable编码的实现方法 |
---|---|---|
HSET | 先调用ziplistPush函数,将键推入到压缩列表表尾,然后再次调用ziplistPush函数,将值推入到压缩列表表尾 | 调用dictAdd函数,将新节点添加到字典里面 |
HGET | 首先调用ziplistFind函数,在压缩列表中查找指定键对应节点,然后调用ziplistNext函数,将指针移动到键节点旁边的值节点,最后返回值节点。 | 调用 dictFind 函数, 在字典中查找给定键, 然后调用 dictGetVal 函数, 返回该键所对应的值。 |
HEXISTS | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 | 调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 |
HDEL | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 | 调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。 |
HLEN | 调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。 | 调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。 |
HGETALL | 遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。 | 遍历整个字典, 用 dictGetKey 函数返回字典的键, 用 dictGetVal 函数返回字典的值。 |
# 1.赋值命令
# 将一个或多个值拆入到列表头部(从左侧添加)
lpush key value1 [value2] ...
# 在列表中添加一个或多个值(从右侧添加)
rpush key value1 [value2] ...
# 将一个值插入到已存在列表头部,如果列表不在,操作无效
lpushx key value
# 一个值插入已存在的列表尾部(最右),如果列表不在,操作无效
rpushx key value
# 2.取值语法
# 获取列表长度
llen key
# 通过索引获取列表中的元素
lindex key index
# 获取列表指定范围内的元素
lrange key start stop
# 3. 删除语法
# 移除并获取列表的第一个元素(从左侧删除)
lpop key
# 移除列表的最后一个元素,返回值为移除的元素
rpop key
# 移除并获取列表第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
blpop key1 [key2] timeout
e.g.:
127.0.0.1:6379[1]> blpop l2 3
1) "l2"
2) "a"
127.0.0.1:6379[1]> blpop l2 3
1) "l2"
2) "b"
127.0.0.1:6379[1]> blpop l2 3
(nil)
(3.09s)
# brpop 相同,只不过是最后一个元素
# 对一个列表进行修剪,让列表只保留指定区间内的元素,不再指定区间之内元素都将被删除
ltrim key start stop
e.g.:
127.0.0.1:6379[1]> lrange l1 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
127.0.0.1:6379[1]> ltrim l1 0 3
OK
127.0.0.1:6379[1]> lrange l1 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
# 4.修改语法
# 通过索引设置列表元素的值
lset key index value
# 在列表元素前或者后插入元素
linsert key before|after world value
# 在key为l2的world为d位置之前插入bbbb before after
linsert l2 before d bbbb
# 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
rpoppush source destination
e.g.:
127.0.0.1:6379[1]> lrange l1 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
127.0.0.1:6379[1]> lrange l2 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379[1]> rpoppush l1 l2
(error) ERR unknown command ‘rpoppush‘
# 把l1中最右侧数据也就是‘a‘ 添加到l2中最左侧
127.0.0.1:6379[1]> rpoplpush l1 l2
"a"
127.0.0.1:6379[1]> lrange l1 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
127.0.0.1:6379[1]> lrange l2 0 -1
1) "a"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"
# 应用订单队列。
1.对数据量大的集合进行删减
列表数据显示,关注列表,粉丝列表,留言评论等..分页,热点新闻等
利用lrange还可以很方便实现分页功能,在博客系统中,每个博文评论也可以存入一个单独list中。
Redis中列表对象比较特殊,在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的,但是在版本3.2只有,重新引入了一个quicklist数据结构,列表底层都是由quicklist实现。
老版本总,列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
1.列表对象保存的所有字符串元素的长度都小于64字节;
2.列表对象保存元素数量小于512个;
不能满足这两个条件条件列表对象需要使用linkedlist编码,当这两个条件任何一个不满足的时候,就会有一个格式的转换。
list结构:
/* Structure to hold list iteration abstraction. */
typedef struct {
robj *subject;
unsigned char encoding;
unsigned char direction; /* Iteration direction */
quicklistIter *iter;
} listTypeIterator;
/* Structure for an entry while iterating over a list. */
typedef struct {
listTypeIterator *li;
quicklistEntry entry; /* Entry in quicklist */
} listTypeEntry;
List结构定义一个列表头结点,以及迭代器指针,指针中指定了编码格式和迭代方向。
list命令:
命令 | 说明 |
---|---|
BLPOP key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
BRPOP key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
BRPOPLPUSH source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如但果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
LINDEX key index | 通过索引获取列表中的元素 |
LINSERT key BEFORE | AFTER pivot value |
LLEN key | 获取列表长度 |
LPOP key | 移出并获取列表的第一个元素 |
LPUSH key value1 [value2] | 将一个或多个值插入到列表头部 |
LPUSHX key value | 将一个或多个值插入到已存在的列表头部 |
LRANGE key start stop | 获取列表指定范围内的元素 |
LREM key count value | 移除列表元素 |
LSET key index value | 通过索引设置列表元素的值 |
LTRIM key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
RPOP key | 移除并获取列表最后一个元素 |
RPOPLPUSH source destination | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
RPUSH key value1 [value2] | 在列表中添加一个或多个值 |
RPUSHX key value | 为已存在的列表添加值 |
3.3版本之后,重新引入了quicklist数据结构,列表底层都由quicklist实现。
相比于双向链表linkedlist便于在两端进行push和pop操作,在插入节点上复杂度低,但内存开销比较大,因为它在每个节点上除了要保存数据之外,还要额外保存两个指针;并且双向链表各个节点是单独的内存块,地址不连续,节点容易产生碎片。
而ziplist存储在一段连续内存上,所以存储效率很高,但不利于修改操作,插入和删除操作需要频繁的申请和释放内存,特别当ziplist长度很长时候,一次realloc(改变内存区域的大小)可能会导致大批量数据拷贝。
而quicklist我们仍可以将其看做一个双向列表,但是列表每个节点都是一个ziplist,其实就是linkedlist和ziplist结合,quicklick中每个节点ziplist能够存储多个数据元素。
quicklist定义如下:
typedef struct quicklist {
quicklistNode *head; // 指向quicklist的头部
quicklistNode *tail; // 指向quicklist的尾部
unsigned long count; // 列表中所有数据项的个数总和
unsigned int len; // quicklist节点的个数,即ziplist的个数
int fill : 16; // ziplist大小限定,由list-max-ziplist-size给定
unsigned int compress : 16; // 节点压缩深度设置,由list-compress-depth给定
} quicklist;
可以看到,这边其实有两个统计值,count用来统计所有数据项的个数总和,len用来统计quicklist的节点个数, 因为每个节点ziplist都能存储多个数据项,所以有了这两个统计值。
另外,插一点,quicklist的这个结构体在源码中说是占用了32byte的空间,怎么计算的呢?这边涉及到了位域的概念,所谓”位域“是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。比如这个“int fill : 16” 表示不用整个int存储fill,而是只用了其中的16位来存储。
quicklist的节点node的定义如下:
typedef struct quicklistNode {
struct quicklistNode *prev; // 指向上一个ziplist节点
struct quicklistNode *next; // 指向下一个ziplist节点
unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
unsigned int count : 16; // 表示ziplist中的数据项个数
unsigned int encoding : 2; // 编码方式,1--ziplist,2--quicklistLZF
unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2--ziplist
unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
unsigned int attempted_compress : 1; // 测试相关
unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;
redis的集合对象set底层存储结构特别神奇,底层使用了intset和hashtable 两种数据结构存储,intset我们可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。
intset内部其实是一个数组(int8_t coentents数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。
set常见命令
# 1.赋值语法
sadd key member1 [member2] 向集合添加一个或多个成员
# 2.取值语法
# 获取集合的成员数
scard key
# 返回集合中所有成员
smembers key
# 判断member元素是否是集合key的成员(开发中:验证是否存在判断)
sismember key member
e.g.:
# 返回1为存在,0表示不存在
127.0.0.1:6379[1]> sismember s1 a
(integer) 1
127.0.0.1:6379[1]> sismember s1 d
(integer) 1
127.0.0.1:6379[1]> sismember s1 z
(integer) 0
# 返回集合中一个或多个随机数
srandmember key [count]
e.g.:
127.0.0.1:6379[1]> srandmember s1 2
1) "c"
2) "d"
127.0.0.1:6379[1]> srandmember s1 3
1) "a"
2) "b"
3) "d"
# 用它可以随机抽奖功能
# 3.删除语法
# 移除集合中一个或对个成员
srem key member1 [member2]
# 移除并返回集合中一个随机元素
spop key [count]
e.g.:
# 返回 删除的value
127.0.0.1:6379[1]> spop s1 1
1) "d"
# 将member元素从source集合移动到destination集合
smove source destination member
e.g.:
127.0.0.1:6379[1]> smove s1 s2 c
# 将集合s1 中 c的值 移动到 s2 集合内。
# 4.差集语法
# 返回给定所有集合的差距(左侧为准)
sdiff key1 [key2]
# 5.交集语法
# 返回给定所有集合的交集
sinter key1 [key2]
# 并集语法
# 返回所有给定集合的并集
sunion key1 [key2]
# 所有给定集合并集存储在 destination 集合中
sunionstore destination key1 [key2]
# 所有给定集合交集储存在 destination 集合中
sinterstore destination key1 [key2]
# 所有给定集合差集储存在 destination 集合中
sdiffstore destination key1 [key2]
共同喜好,二度好友等功能,对上面所有集合操作。你还可以使用不同命令选择将结果返回给客户端还是存储到一个新的集合中。
利用唯一性,可以统计访问网站所有独立IP
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
第一个添加到集合元素决定初始存储方式,如果第一个原始是一个整数,初始的编码就会是REDIS_ENCODING_INTSET,否则就是REDIS_ENCODING_HT,所以集合中数据操作时候基本都涉及到两个存储方式判断。
set结构:
typedef struct redisObject {
unsigned type:4; // 此字段为OBJ_SET
unsigned encoding:4; // 如果是set结构,编码为OBJ_ENCODING_INTSET或OBJ_ENCODING_HT
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
相同的是,set结构也存储redisObject结构体,通过指定type=OBJ_SET来确定这是一个集合对象,当是一个接个对象的时候,配套的endoding只能有对应两种取值。
set命令:
命令 | 说明 |
---|---|
sadd | 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略 |
scard | 返回集合 key 的基数(集合中元素的数量) |
spop | 移除并返回集合中的一个随机元素 |
smembers | 返回集合 key 中的所有成员 |
sismember | 判断 member 元素是否集合 key 的成员 |
sinter | 返回多个集合的交集,多个集合由 keys 指定 |
srandmember | 返回集合中的一个随机元素 |
srandmember | 返回集合中的 count 个随机元素 |
srem | 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略 |
sunion | 返回多个集合的并集,多个集合由 keys 指定 |
sdiff | 返回一个集合的全部成员,该集合是所有给定集合之间的差集 |
集合编码转换
如果一个集合使用REDIS_ENCODING_INTSET编码,那么当以下任何一个条件被满足时,这个集合会被转换成REDIS_ENCODING_HT编码。
redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员
不同的是每个元素都会关联一个double类型的分数(score),redis正式通过分数来为集合中的成员进行从小到大的排序
有序集合的成员是唯一的,但分数却可以重复
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1),集合中最大的成员数为2^32-1
zset 常用语法
# 1.赋值语法
zadd key score1 member1 [score2 member2]
e.g.:
127.0.0.1:6379[1]> zadd z1 70 java 100 python 67 db
# 2.取值语法:
# 获取有序集合的成员数
zcard key
# 计算在有序集合中指定区间分数的成员数
zcount key min max
e.g.:
# 查看有序集合z1中 double 值在0-80的个数
127.0.0.1:6379[1]> zcount z1 0 80
(integer) 2
# 返回有序集合中指定成员的索引
127.0.0.1:6379[1]> zrank z1 python
(integer) 2
# 通过索引区间返回有序集合成指定区间内成员(低到高)
zrange key start stop
e.g.:
127.0.0.1:6379[1]> zrange z1 0 -1
1) "db"
2) "java"
3) "python"
# 返回有序集中指定区间内的成员,通过索引,分数从高到底
e.g.:
127.0.0.1:6379[1]> zrevrange z1 0 -1
1) "python"
2) "java"
3) "db"
# 删除语法
# 删除指定key的集合
del key
# 删除有序集合中一个或多个成员
zrem key member [member ...]
# 移除有序集合中给定排名区间的所有成员(第一名是0)
zremrangebyrank key start stop# 通过下标索引删除
# 移除有序集合中给定分数区间的所有成员
zremrangebyscore key min max# 根据double分数删除
排行榜
还可以用来做权重排序
/* RedisObject结构 */
typedef struct redisObject {
unsigned type:4; // OBJ_ZSET表示有序集合对象
unsigned encoding:4; // 编码字段为OBJ_ENCODING_ZIPLIST或OBJ_ENCODING_SKIPLIST
unsigned lru:LRU_BITS; // LRU_BITS为24位
int refcount;
void *ptr; // 指向数据部分
} robj;
同其他对象一样,zset结构也是存储在redisObject结构体中,通过指定type=OBJ_ZSET来确定这是一个有序集合对象,当是一个有序集合对象时候,配套的endoding只能有对应两种值。
ziplist编码有序集合
当用REDIS_ENCODING_ZIPLIST作为有序集合的底层编码时,有序集合中的元素按score值从小到大排序,先保存value,在保存score,示意图如下:
|<-- element 1 -->|<-- element 2 -->|<-- ....... -->|
+---------+---------+--------+---------+--------+---------+---------+---------+
| ZIPLIST | | | | | | | ZIPLIST |
| ENTRY | member1 | score1 | member2 | score2 | ... | ... | ENTRY |
| HEAD | | | | | | | END |
+---------+---------+--------+---------+--------+---------+---------+---------+
skiplist编码的有序集合
当有序集合用REDIS_ENCODING_SKIPLIST编码的时候,并不仅仅是用了跳跃表skiplist,还用到了字典dict。为什么要这么做呢。 跳跃表的优点是能够在O(log_N_)的期望时间内根据分值score对元素进行定位,对于一些范围查找命令,比如ZRANGE能够较好的支持。
但是如果要取出对应元素的分值,或者查看没有某个元素是否在有序集合内,单纯靠跳跃表就不够用了, 因为跳跃表是根据score组织的,不是根据元素值组织的。所以在有序集合中另外用了一个dict来支持这些操作。dict中key就是元素的值,value就是score。这样就能够在O(1)的复杂度内找到对应的分值,或者判断一个元素是否在有序集合中。
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
zset编码转换
如果一个有序集合一开始是用压缩列表REDIS_ENCODING_ZIPLIST做为底层编码,只要满足下边的条件,就会将底层编码转换为REDIS_ENCODING_SKIPLIST
zset命令:
命令 | 说明 |
---|---|
ZADD key score member [[score member] [score member] …] | 将一个或多个 member 元素及其 score 值加入到有序集 key 当中 |
zcard | 返回有序集 key 的基数 |
ZCOUNT key min max | 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量 |
ZINCRBY key increment member | 为有序集 key 的成员 member 的 score 值加上增量 increment |
zrange | 返回有序集 key 中,指定区间内的成员 |
zrevrange | 返回有序集 key 中,指定区间内的成员 |
zrangeByScore | 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员 |
zrank | 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列 |
zrevrank | 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序 |
zrem | 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略 |
zscore | 返回有序集 key 中,成员 member 的 score 值 |
redis发布订阅是一种消息通信模式,发布者发送消息,订阅者接收消息。redis客户端可以订阅任意数量频道。
如下图,下列三个客户端对当前频道进行订阅:
当有消息通过publish命令发送给频道channel,这个消息就会被发送给订阅它的三个客户端。
常用命令:
# 订阅频道
# 订阅给定一个或多个频道信息
subscribe channel [channel...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern]
# 发布频道
# 将信息发送到指定频道
publish channel message
# 退订频道
# 退订给定频道
unsubscribe [channel]
# 退订所有给定模式频道
punsubscribe [pattern]
示例:
# client 关注频道 qtv:
subscribe qtv
# 向 qtv 发布消息
publish qtv newmessage
1.在一个博客网站中,有100个粉丝订阅了你,当你发布新文章,就可以推送消息给粉丝们。
2.微信公众号模式
redis下,数据是由一个整数索引表示,而不是由一个数据库名称。默认情况下,一个客户端连接到数据库0.
redis配置文件中下面参数来控制数据总数:
database 16 # 从0开始...15
切换数据库
select 数据库
移动数据
# 将当前key 移动到另一个数据库
move key 名称 数据库
数据库清空
flushdb # 清除当前数据的说有key
flushall # 清除整个redis数据库所有key
可以一次性执行多个命令(允许在一次单独的步骤中执行一组命令)。并有以下两个重要保证:
批量操作在发送exec命令前被放入队列缓存,收到exec命令后进入事务执行,事务中任意命令执行失败,其余命令依然被执行。在事务执行过程中,其他客户端提交的命令请求不会插入到事务执行命令序列中。
1.redis会将一个事务中所有命令序列化,然后按顺序执行
2.执行中不会被其他命令插入,不许出现加塞现象。
事务从开始到执行三个阶段:
开始事务--->命令入列--->执行事务
事务常用命令:
DISCARD 取消事务,放弃执行事务块内的所有命令
EXEC 执行所有事务块的命令
MULTI 标记一个事务块的开始
UNWATCH 取消WATCH命令对所有key的监视
WATCH key [key...] 监视一个或多个key,如果在事务执行之前这个key被其他命令所改动,那么事务将被打断
案例转账功能:
A有80元
B有10元
A-->B转50元
#创建账户A=80元和账户B=10元
set account:a 80
set account:b 10
# 1.示例:1
multi # 标记事务开始
get account:a
get account:b
decrby account:a 50
incrby account:b 50
exec #执行所有事务。
# 2.示例:2
# 取消事务,事务的回滚
discard
# 3.示例:3 事务的错误处理
# 如果执行某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
# 4.示例:
# 队列中某个语法写错,执行时整个所有队列都会被取消
# 5.示例5:
# 某一账户在事务内进行操作,在提交事务前,另一个进程对该账户进行操作,将打断事务。
# 场景a向b转10元
# 创建账户a 10元
127.0.0.1:6379[2]> set account:a 10
OK
# 创建账户b 0元
127.0.0.1:6379[2]> set account:b 0
OK
# 监听a变化
127.0.0.1:6379[2]> watch account:a
OK
# 开启事务
127.0.0.1:6379[2]> multi
OK
# a减10元
127.0.0.1:6379[2]> decrby account:a 10
QUEUED
# b添加10元
127.0.0.1:6379[2]> incrby account:b 10
QUEUED
# 此时另一个进程减少a账户5元
127.0.0.1:6379[2]> decrby account:a 5
(integer) 5
# 然后提交事务
127.0.0.1:6379[2]> exec
# 由于在第一个事务中a发生变化,那么此事务不会执行成功
(error) EXECABORT Transaction discarded because of previous errors.
# a变为5元
127.0.0.1:6379[2]> get account:a
"5"
# b还是0元
127.0.0.1:6379[2]> get account:b
"0"
1.输入Multi命令开始,输入的命令都会依次进入队列中,但不会执行。
2.知道输入exec后,redis会将之前命令队列中命令依次执行
3.命令队列的过程中可以通过discard来放弃队列运行
应用场景商品秒杀。
在reids中允许用户设置最大使用内存大小:
e.g.:
maxmemory 512G
1.volatile-lru:设定超时时间的数据中,删除最不常用数据
2.allkeys-lru:查询所有key中最近不常使用的数据进行删除,这是应用最广泛的策略
3.volatile-random:在已经设定了超时的数据中随机删除
4.allkeys-random:查询所有key之后随机删除
5.volatile-ttl:查询全部设定超时时间的数据,之后排序,将马上将要过期的数据进行删除操作
6.Noevication:如果设置为该属性,则不会进行删除操作,如果内存溢出则报错返回
7.volatile-lfu:从所有配置过期时间的键中驱逐使用频率最少的键
8.allkeys-lfu:从所有键中驱逐使用频率最少的键
1.setnx(key,value) ‘set if not exists‘ 若该key-value不存在,则成功加入缓存并返回1,否则返回0
2.get(key) 先获取key对应的value值,若不存在则返回nil.
3.getset(key,value) 先获取key对应value值,弱不存在则返回nil,然后将旧的value更新为新的value.
4.expire(key,seconds) 设置key-value有效时间
#在实现时候要注意几个关键点:
1.锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;
2.同一时刻只能由一个线程获取到锁。
#实现方法:
如果获取锁成功,那么按照流程执行业务逻辑,执行完毕,删除锁。如果没有获取到锁,继续判断时间戳,看是否可以重置并获取到锁,先get得到当前的值,并且比较一下当前的系统时间和得到的值的大小,如果当前时间大于值。说明已经过期了,接下来执行getset操作,将key的值重新设置一下,如果返回的旧值不存在,说明这个key已经被删除了。或者这个旧值存在,并且和之前get的值相同,说明重置锁的过程中,锁没有发生变化,此时可以获取到锁了,接下来执行相应业务逻辑
默认持久化机制。RDB相当于快照保存一种状态。几十G数据--->几KB快照。
这种方式就是将内存中数据以快照方式写入二进制文件,默认文件名为dump.rdb
优缺点
优点:
快照保存数据快,还原数据快。适用于灾难备份
缺点:
小内存机器不适合使用,RDB机制符合要求就会快照
由于快照方式在一定间隔时间做一次,所以如果redis意外down掉。就会丢失最后一次快照后所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化。
Append-only file:aof比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof),当redis重启时,会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
有三种方式:
# appendonly yes 启动aof持久化方式
1.appendfsync always 收到写命令就立即写入硬盘,最慢,但是保证数据持久化
2.appendsync everysec 每秒钟写入磁盘一次,在性能和持久化方面做了很好折中
3.appendfsync no 完全依赖os.性能最好,持久化没保证
产生问题:
缓存穿透:
查询一个不存在的数据,由于缓存不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决方法:在持久层查不到就缓存空结果。查询时先判断缓存中是否exists,如果有直接返回空,没有查询后返回。
# 此处注意,insert时清除查询key,否则即便DB中有值也查询不到(当然也可以设置空缓存的过期时间)
缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成缓存雪崩。
这个没有完美解决方法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑加锁或队列方式保证缓存的单线程(进程)写,从而避免失效的大量的并发请求落到底层存储系统上。
实时同步
对强一致要求比较高,应采用实时同步方案,即查询缓存,查询不到再从DB查询。保存到缓存,更新缓存储,先更新数据库。再将缓存的设置过期(建议不要去更新缓存内容,直接设置缓存过期)
异步队列
对于并发程度较高,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。
使用阿里的同步工具
canal实现方式是模拟mysql slave 和master 的同步机制,监控DB bitlog的日志更新来触发缓存的更新,此种方法可以解放程序员双手,减少工作量,但是使用时存在局限性:
采用UDF自定义函数的方式
面对mysql API进行编程,利用触发器进行缓存同步,但UDF主要C/C++语言实现,学习成本高。DML触发器(数据操作语言)是指触发器在数据库中发生DML事件时启动,DML事件即指在表或试图中修改数据库的insert,update,delete语句。
Lua脚本
定时任务:每天redis中数据更新到数据库(非实时同步)
热点key
热点key:某个key访问非常频繁,当key失效的时候又大量线程来构建缓存,导致负载增加,系统崩溃
解决办法:
1.使用锁:单机用synchronized,lock等,分布式用分布式锁
2.缓存过期时间不设置,而是设置在key对应的value里,如果检测到存储的时间超过过期时间,则异步更新缓存。
3.在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。
4.设置标签缓存,标签缓存设置过期时间,标签缓存过期后,需异步地更新实际缓存。
redis可能存在问题:
一般来说要将redis运用于工程项目中,只使用一台redis是万万不能的,原因如下:
1。从结构上单个redis服务器会发生单点故障。并且一台服务器需要处理所有的请求负载,压力较大。
2.从容量上,单个redis服务器内存容量有限,就算一台redis服务内容容量为256G,也不能将所有内容用做redis存储内存,一般来说单台redis最大使用内存不用超过20G
保持高可用
高可用性,通常来描述一个系统经过过专门设计,从而减少停工时间,而保持其服务的高可用性(一直都能用)
高并发:
高并发是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指。通过设计保证系统能够同时并行处理很多请求
高并发相关常用的一些指标有响应时间,吞吐量,每秒查询率QPS,并发用户数等
响应时间:一般系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
吞吐量:单位时间内处理的请求数量
QPS:每秒响应请求数,在互联网领域,这个指标和吞吐量区分没有特别明显
提升系统并发能力
提高系统并发能力方式,方法论上主要有两种:
垂直扩展与水平扩展
1.垂直扩展
提升单机处理能力,垂直扩展的方式又有两种:
- 增强单机硬件性能,例如:增加CPU核数如32核。升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充内存如128G
- 提升单机架构性能,例如使用Cache来减少IO次数,使用异步来增加单服务吞吐量。使用无锁数据结构来减少响应时间。
在互联网业务发展非常迅猛早期,如果预算不是问题,强烈建议使用 "增强单机硬件性能"的方式提升系统并发能力,因为这个阶段,公司的战略往往是发展业务抢时间。而 "增强单机硬件性能" 往往是最快方法。
2.水平扩展
水平扩展,只要增加服务器数量,就能线性扩充系统性能,水平扩展对系统架构设计是有要求的,难点在于:如何在架构各层进行各层进行可水平扩展设计
转载原文链接:https://blog.csdn.net/harleylau/article/details/80612616
原文:https://www.cnblogs.com/xujunkai/p/12638864.html