初识redis
一、概述
1.1 什么是redis?
Redis是用C语言开发的一个开源的高性能键值对(key-value)存储结构的内存级非关系型(NoSQL)数据库。
-
-
- 支持多种数据存储格式
- 支持持久化
- 支持集群
-
官方提供测试数据,50个并发执行100000个请求,读的速度110000次/s,写的速度81000次/s,是已知最快的k-v DB。
且Redis通过提供多种键值数据类型来适应不同场景下的存储需求。
目前为止Redis支持的键值数据类型如下:
1) 字符串类型 string
2) 哈希类型 hash
3) 列表类型 list
4) 集合类型 set
5) 有序集合类型 sortedset
小贴士:
- 什么是NoSQL?
NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,是一项全新的数据库理念,泛指非关系型的数据库。
随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。
- NoSQL和关系型数据库比较
* NoSQL优点:
1)成本:nosql数据库简单易部署,基本都是开源软件,不需要像使用oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。
2)查询速度:nosql数据库将数据存储于缓存之中,关系型数据库将数据存储在硬盘中,自然查询速度远不及nosql数据库。
3)存储数据的格式:nosql的存储格式是key-value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。
4)扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。而nosql是基于键值对,数据之间没有耦合性,所以非常容易水平扩展。
5)性能:NOSQL是基于键值对的,可以想象成表中主键和值的对应关系,而且不需要经过SQL层解析,所以性能非常高。* NoSQL缺点:
1)维护的工具和资料有限,因为nosql是属于新的技术,不能和关系型数据库10几年的技术同日而语。
2)不提供对sql的支持,如果不支持sql这样的工业标准,将产生一定用户的学习和使用成本。
3)不提供关系型数据库对事务的处理。* 关系型数据库的优势:
1)复杂查询可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。
2)事务支持使得对于安全性能很高的数据访问要求得以实现。* 总结:
关系型数据库与NoSQL数据库并非对立而是互补的关系,即通常情况下使用关系型数据库,在适合使用NoSQL的时候使用NoSQL数据库,让NoSQL数据库对关系型数据库的不足进行弥补。
一般会将数据存储在关系型数据库中,在nosql数据库中备份存储关系型数据库的数据。
- 主流的NoSQL产品
• 键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
• 列存储数据库
相关产品:Cassandra, HBase, Riak
典型应用:分布式的文件系统
数据模型:以列簇式存储,将同一列数据存在一起
优势:查找速度快,可扩展性强,更容易进行分布式扩展
劣势:功能相对局限
• 文档型数据库
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势:数据结构要求不严格
劣势: 查询性能不高,而且缺乏统一的查询语法
• 图形(Graph)数据库
相关数据库:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案
1.2 为什么用redis?
在项目中,主要考虑两个角度:性能和并发。
- 性能:我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
- 并发:在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。
1.3 redis的应用场景
• 缓存(数据查询、短连接、新闻内容、商品内容等等热点数据的缓存)
• 聊天室的在线好友列表
• 任务队列。(秒杀、抢购、12306等等限时业务的运用)
• 应用排行榜。(排行榜相关问题)
• 网站访问统计。(计数器相关问题)
• 数据过期处理(可以精确到毫秒)
• 分布式集群架构中的session分离
• 分布式锁
二、Redis的下载和安装
2.1 下载
2.2 安装
直接解压即可,目录如下图:
2.3 启动
三、命令行操作数据
redis存储的是:key-value格式的数据,其中key都是字符串,value有5种不同的数据结构。
- value的数据结构及其命令操作
1) 字符串类型 string :最多可以容纳数据长度512M
命令 | 描述 | 命令行 |
set | 赋值 | set key value |
get | 获取值 | get key |
del | 删除值 | del key |
append | 追加字符串 | append key value |
incr | 自增(+1) | incr key |
decr | 自减(-1) | decr key |
incrby | 自增,增加指定步长 | incrby key increment |
decrby | 自减,减少指定步长 | decrby key increment |
getset | 先获取元素再赋值 | getset key value |
setex | 赋值并指定key的存活时间 | setex key second value |
setnx | Key不存在就赋值,否则不做操作 | setnx key value |
strlen | 返回key的值的长度 | strlen key |
mset | 同时设置多个key value | mset key1 value1[key2 value2] |
mget | 同时获取多个key的值 | mget key1[key2] |
2) 哈希类型 hash :map格式。它是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。每个hash可以存储232 - 1 键值对(40多亿)
命令 | 描述 | 命令行 |
hset | 给key中的field字段赋值 | hset key field value |
hget | 获取key中field字段的值 | hget key field |
hgetall | 获取key中所有的field和value | hgetall key |
hdel | 删除key中的field字段 | hdel key field1[field2] |
hexists | 判断key中field的数量 | hlen key |
hmset | 同时设置多个field和value | hmset key field1 value1 [field2 value2] |
hmget | 同时获取多个field的值 | hmget key field1 [field2] |
hincrby | 给field的值增加指定的步长 | hincrby key field increment |
hkeys | 获取所有的key | hkeys keys |
hvals | 获取所有的value | hvals key |
3) 列表类型 list :linkedlist格式。支持重复元素,常用的操作是添加一个元素到列表的头部(左边)或者尾部(右边),或者获得列表的某一个片段
命令 | 描述 | 命令行 |
lpush | 在list头部(左边)添加值 | lpush key value1[value2…] |
lpushx | Key存在头部(左边)添加,否则不做操作 | lpushx key value1 |
rpush | 在list尾部(右边)添加值 | rpush key value1[value2…] |
rpushx | Key存在尾部(右边)添加,否则不做操作 | rpushx key value1 |
lrange |
获取指定位置的数据(范围获取) * lrange key 0 -1(-1代表所有,从0开始到末尾获取所有) |
lrange key start end |
lpop | 从头部(左边)弹出key的值(删除),并将元素返回 | lpop key |
rpop | 从尾部(右边)弹出key的值(删除),并将元素返回 | rpop key |
rpoplpush | 从一个list的尾部弹出插入到另一个list的头部 | rpoplpush source destnation |
llen | 返回key的长度 | llen key |
lrem | 删除签名几个值为value的元素 | lrem key count value |
lset | 按下标赋值 | lset key count value |
lindex | 返回下标的值 | lindex key index |
ltrim | 截取list指定位置的值 | ltrim key start end |
linsert | 在某个元素前面或后面插入值 | linsert key before|after pivot value |
4) 集合类型 set :不允许重复元素,无序
命令 | 描述 | 命令行 |
sadd | 添加值 | sadd key value1[value2…] |
smembers | 遍历集合,获取值 | smembers key |
srem | 删除某个指定成员 | srem key value1[value2…] |
scard | 获取key的成员数量 | scard key |
sismember | 判断成员是否存在 | sismember key value |
spop | 随机弹出一个值(删除) | spop key |
srandmember | 随机弹出一个成员(不删除) | srandmember key |
smove | 移动一个集合的成员到另一个集合 | smove source destination value |
sdiff | 求集合的差集 | sdiff key [key…] |
sdiffstore | 求集合的差集并存到新集合中 | sdiffstore destination key [key …] |
sinter | 求集合交集 | sinter key [key…] |
sunion | 求集合并集 | sunion key [key…] |
sunionstore | 求集合并集并存到新集合中 | sunionstore destination key[key…] |
5) 有序集合类型 sortedset :
-
- 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。但元素有顺序。
- 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
- 有序集合的成员是唯一的,但分数(score)却可以重复。
命令 | 描述 | 命令行 |
zadd | 添加成员(score分数) | zadd key score value1[score value2…] |
zrange |
遍历指定下标之间的成员[及分数] (分数从小到大排列) |
// withscores根据关联的分数进行从小到大的排序 zrange key start end [withscores] |
zrem | 删除指定成员 | zrem key value1[value2…] |
zcard | 获取key的成员数量 | zcard key |
zcount | 获取key指定分数直接的成员数量 | zcount key min max |
zrank | 返回成员的下标(分数从小到大排) | zrank key value |
zincrby | 给成员增加指定分数 | zincrby key increment value |
zrevrank | 返回成员下标(分数从大到小) | zrevrank key value |
zscore | 获取指定成员的分数 | zscore key value |
zrevrangebyscore | 获取在指定分数之间的成员 | zrevrangebyscore key max min [withscore] |
- 通用命令
keys * // 查询所有的键
type key // 获取键对应的value的类型
del key // 删除指定的key value
四、持久化
redis是一个内存数据库,当redis服务器重启,或者电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中。
- redis持久化机制
1)RDB:默认方式,不需要进行配置,默认就使用这种机制。
-
- 在一定的时间间隔内,检测key的变化情况,然后持久化数据
① 编辑redis.windows.conf文件,可以根据需求修改以下数据
# after 900 sec (15 min) if at least 1 key changed (15分钟内至少有一个key发生变化才会持久化)
save 900 1
# after 300 sec (5 min) if at least 10 keys changed (5分钟内至少有十个key发生变化才会持久化)
save 300 10
# after 60 sec if at least 10000 keys changed (1分钟内至少有一万个key发生变化才会持久化)
save 60 10000
② 重新启动redis服务器,并指定配置文件名称
③ 持久化最终的结果会生成一个dump.rdb文件
2)AOF:日志记录的方式,可以记录每一条命令的操作。
-
- 可以每隔一秒或者每操作一次命令后,持久化数据。(影响性能,不推荐)
① 编辑redis.windwos.conf文件,可以根据需求修改以下数据
appendonly no(关闭aof) --> appendonly yes (开启aof)
appendfsync always (每一次操作都进行持久化)
# appendfsync everysec (每隔一秒进行一次持久化)
# appendfsync no (不进行持久化)
五、redis事务特征
1、 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事务中的所有命令被原子的执行。
2、 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
3、 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句。
4、 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
5、 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。
六、常用的分布式缓存
常用的分布式缓存包括redis、memcached和阿里的Tair,因为redis提供的数据结构比较丰富且简单易用,所以redis使用广泛。
6.1 缓存之间的区别
6.1.1 Redis VS memcached
- 存储方式:Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis有部份存在硬盘上,这样能保证数据的持久性;
- 数据支持类型:Memcache对数据类型支持相对简单。Redis有复杂的数据类型;
- 使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
6.2 常见的主要问题
6.2.1 缓存与数据库双写一致性问题
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
-
- 备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。
- 双写不停机迁移方案
双写迁移方案的核心在双写,首先要修改系统所有需要写库的地方,所有对数据的写操作不但要写入旧库,也要同时写入新库。
然后使用写好的数据迁移程序,去读取老数据库的数据写入到新的数据库里面去,写的时候要根据数据的最后更新时间去判断数据,如果读出来的数据新库没有直接写入,如果新库也有,查看最后更新时间,旧库的新就覆盖写入,如果新库的新就放弃这条数据。
导完一轮数据之后,有可能数据还是存在不一致,那么就写个程序做一轮校验,对比老库和新库的每条数据,如果存在不一样的,就针对这些不一样的,再次去进行数据同步。反复循环,直到数据完全一致。
接着当数据完全一致了,就ok了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干了。
6.2.2 缓存击穿问题
- 概述:缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。
- 解决方案:
- 使用互斥锁排队:普通做法,根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。
- 布隆过滤器(推荐):bloomfilter就类似于一个hash set,用于快速判断某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。
6.2.3 缓存雪崩问题
- 概述:缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。
- 解决方案:
- 使用互斥锁
- 建立备份缓存:缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有那就读B,并且更新A缓存和B缓存。
6.2.4 缓存的并发竞争问题
- 问题举例:两个连接同时对price进行写操作,同时加10,最终结果我们知道,应该为30才是正确。
考虑到一种情况:
T1时刻,连接1将price读出,目标设置的数据为10+10 = 20;
T2时刻,连接2也将数据读出,也是为10,目标设置为20;
T3时刻,连接1将price设置为20;
T4时刻,连接2也将price设置为20,则最终结果是一个错误值20。 - 解决方案:
- 利用redis的incrby(自增,增加指定步长);
- 加锁:这个是针对客户端来的,在代码里要对redis操作的时候,针对同一key的资源,就先进行加锁(java里的synchronized或lock)。
七、异步消息队列
redis 的 list(列表)数据结构常用来作为异步消息队列使用,使用 lpush/rpush 操作入队列,使用 lpop/rpop 来出队列。
客户端通过队列的 pop 操作来获取信息,然后进行处理,处理完了再接着获取消息,再进行处理,如此循环往复,这便是作为队列消费者的客户端的生命周期。
- 问题:如果队列空了,客户端会陷入 pop 死循环,这就是浪费生命的空轮询。不但会拉高客户端的CPU,redis的QPS也会被拉高。
- 解决:使用 sleep 来解决。
- 更好的解决方法:用 blpop/brpop 来代替 lpop/rpop。(注意捕捉异常。前缀字符 b 代表blocking,也就是阻塞读。阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则会立即醒过来,消息的延迟几乎为零。)
小贴士:
消息延时队列:通过 redis 的 zset(有序列表)来实现。
八、springboot整合redis
SpringBoot 整合 redis - 九点的太阳 - 博客园 (cnblogs.com)