概述
互联网架构的演变历程
第 1 阶段:
- 数据访问量不大,简单的架构即可搞定。
- 适合小型项目。
app -> dao -> mysql
第 2 阶段:
- 数据访问量大,使用缓存技术来缓解数据库的压力。
- 不同的业务访问不同的数据库。
- 适合中型项目。
app -> dao -> cache -> [mysql1, mysql2, mysql3]
第 3 阶段:
- 主从读写分离。
- 之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又了。
- 一个数据库负责写,一个数据库负责读;分工合作。
- 让 Master(主数据库)来响应事务性(增删改)操作,让 Slave(从数据库)来响应非事务性(查询)操作,然后再采用主从复制来把 Master 上的事务性操作同步到 Slave 数据库中。
- MySQL 的 Master / Slave 就是网站的标配。
- 适合大型项目。
app -> dao -> cache -> 主库 -> [从库1, 从库2]
第 4 阶段:
- 在 MySQL 的主从复制,读写分离的基础上,MySQL 的主库开始出现瓶颈。
- 由于 MyISAM 使用表锁,所以并发性能特别差。
- 分库分表开始流行,MySQL 也提出了表分区,虽然不稳定,但有了希望。
- 使用 MySQL 集群。
- 适合超大型项目。
app -> dao -> cache -> {[主库 -> (从库1, 从库2)], [主库 -> (从库1, 从库2)]}
Redis 入门介绍
互联网需求的 3 高:高并发,高可扩,高性能。
Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的 NoSQL(Not only SQL)数据库。
NoSQL 非关系型数据库和传统 RDBMS 关系型数据库相比的优势:
- NoSQL 数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。
- 而在关系数据库里,增删字段是一件非常麻烦的事情;如果是非常大数据量的表,增加字段简直就是一个噩梦。
RDBMS
- 高度组织化结构化数据
- 结构化查询语言 SQL
- 数据和关系都存储在单独的表中
- 数据操纵语言,数据定义语言
- 严格的一致性
- 基础事务
NoSQL
- 代表着不仅仅是 SQL
- 没有声明性查询语言
- 没有预定义的模式
- 键值对存储,列存储,文档存储,图形数据库
- 最终一致性,而非 ACID 属性
- 非结构化和不可预知的数据
- CAP 定理
- 高性能,高可用性和可伸缩性
Redis 的常用使用场景:
-
缓存,是 Redis 当今最为人熟知的使用场景;在提升服务器性能方面非常有效;一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在 Redis 中,因为 Redis 是放在内存中,可以很高效的访问。
-
排行榜,在使用传统的关系型数据库(MySQL、Oracle 等)来做这个事儿,非常的麻烦,而利用 Redis 的 SortSet(有序集合)数据结构能够简单的搞定。
-
计算器 / 限速器,利用 Redis 中原子性的自增操作,可以统计类似用户点赞数、用户访问数等,这类操作如果用 MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个 API 的频率,常用的有抢购时防止用户疯狂点击带来不必要的压力。
-
好友关系,利用集合的一些命令,比如求交集、并集、差集等;可以方便搞定一些共同好
友、共同爱好之类的功能。 -
简单消息队列,除了 Redis 自身的发布 / 订阅模式,也可以利用 List 来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压力,完全可以用 List 来完成异步解耦。
-
Session 共享,以 JSP 为例,默认 Session 是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用 Redis 保存 Session 后,无论用户落在那台机器上都能够获取到对应的 Session 信息。
Redis / Memcache / MongoDB 对比
Redis / Memcache / MongoDB 都是 NoSQL 数据库。
Redis 和 Memcache
- Redis 和 Memcache 都是内存数据库;不过 Memcache 还可用于缓存其他东西,例如图片、视频等等。
- Memcache 数据结构单一 key / value;Redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有效的减少网络 IO 的次数。
- 虚拟内存 – Redis 当物理内存用完时,可以将一些很久没用到的 value 交换到磁盘。
- 存储数据安全 – Memcache 挂掉后,数据没了(没有持久化机制);Redis 可以定期保存到磁盘(持久化)。
- 灾难恢复 – Memcache 挂掉后,数据不可恢复;Redis 数据丢失后可以通过 RBD(将 Redis 在内存中的数据库记录定时 dump 到磁盘上进行持久化)或 AOF(将 Redis 的操作日志以追加的方式写入文件)恢复。
Redis 和 MongoDB
- Redis 和 MongoDB 并不是竞争关系,更多的是一种协作共存的关系。
- MongoDB 是一个基于分布式文件存储的数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
- MongoDB 本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免地进行多次查询。
- MongoDB 需要 Redis 或 Memcache 这样的内存数据库来作为中间层进行缓存和加速。
- 比如在某些复杂页面的场景中,整个页面的内容如果都从 MongoDB 中查询,可能要几十个查询语句,耗时很长;如果需求允许,则可以把整个页面的对象缓存至 Redis 中,定期更新;这样 MongoDB 和 Redis 就能很好地协作起来。
分布式数据库 CAP 原理
CAP 简介
传统的关系型数据库事务具备 ACID:
- Atomicity 原子性
- Consistency 一致性
- Isolation 独立性
- Durability 持久性
分布式数据库的 CAP:
- Consistency - 强一致性
All nodes see the same data at the same time,更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性;一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题;从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
- Availability - 高可用性
可用性指 Reads and writes always succeed,即服务一直可用,而且要是正常的响应时间;好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
- Partition Tolerance - 分区容错性
即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务;分区容错性要求能够让应用,虽然是一个分布式系统,但看上去却是一个可以运转正常的整体;比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
CAP 理论
CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的。
因为 P 是必须的,那么需要选择的就是 A 和 C。
在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
-
选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了 C 属性)。
-
选择一致性 C,为了保证数据库的一致性,必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了 A 属性)。
最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,就面临着选择 A - 继续提供服务,但是数据不保证准确,C - 用户处于等待状态,一直等到数据同步完成。
CAP 总结
分区是常态,不可避免,三者不可共存。
可用性和一致性:
- 一致性高,可用性低
- 一致性低,可用性高
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
- CA - 单点集群(非分布式),满足一致性,可用性的系统,通常在可扩展性上不太强大。
- CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
- AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
下载与安装
下载
Redis:http://www.redis.net.cn
图形工具:https://redisdesktop.com/download
安装
虽然可以在安装在 windows 操作系统,但是官方不推荐,所以安装在 Linux 系统中。
1)上传 tar.gz 包 到 /opt
目录下,并解压:
tar -zxvf redis-5.0.4.tar.gz
2)安装 gcc(必须有网络):
yum -y install gcc
忘记是否安装过,可以使用 gcc -v
命令查看 gcc 版本,如果没有安装过,会提示命令不存在。
3)进入 Redis 目录,进行编译:
make
4)编译之后,开始安装:
make install
安装后的操作
后台运行方式
Redis 默认不会使用后台运行,如果需要,修改配置文件 daemonize=yes
,当后台服务启动的时候,会写成一个进程文件运行。
打开配置文件:
vim /opt/redis-5.0.4/redis.conf
注释掉 bind,关闭保护模式,并修改为后台启动:
...
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1
...
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode no
...
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
...
以配置文件的方式启动:
cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
29674:C 02 Oct 2020 02:56:47.338 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
29674:C 02 Oct 2020 02:56:47.338 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=29674, just started
29674:C 02 Oct 2020 02:56:47.338 # Configuration loaded
防火墙开放 Redis 的端口号:
firewall-cmd --zone=public --add-port=6379/tcp --permanent
firewall-cmd --reload
关闭数据库
单实例关闭
redis-cli shutdown
多实例关闭
redis-cli -p 6379 shutdown
常用操作
检测 6379 端口是否在监听
netstat -lntp | grep 6379
检测后台进程是否存在
ps -ef | grep redis
连接 Redis 并测试
redis-cli
ping
Redis 在 linux 支持命令补全(tab)
HelloWorld
# 保存数据
set k1 china
# 获取数据
get kl
测试性能
先 ctrl + c,退出 Redis 客户端
redis-benchmark
执行命令后,命令不会自动停止,需要手动 ctrl+c 停止测试
====== PING_INLINE ======
100000 requests completed in 2.52 seconds
50 parallel clients
3 bytes payload
keep alive: 1
62.39% <= 1 milliseconds
99.95% <= 2 milliseconds
99.97% <= 3 milliseconds
100.00% <= 3 milliseconds
39682.54 requests per second
...
默认 16 个数据库
vim /opt/redis-5.0.4/redis.conf
...
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16
...
127.0.0.1:6379> get k1 # 查询 k1
"china"
127.0.0.1:6379> select 16 # 切换 16 号数据库
(error) ERR DB index is out of range # 数据库的下标超出了范围
127.0.0.1:6379> select 15 # 切换 15 号数据库
OK
127.0.0.1:6379[15]> get k1 # 查询 k1
(nil)
127.0.0.1:6379[15]> select 0 # 切换 0 号数据库
OK
127.0.0.1:6379> get k1 # 查询 k1
"china"
数据库键的数量
dbsize
清空数据库
清空当前库
flushdb
清空所有(16个)库,慎用
flushall
模糊查询(keys)
模糊查询 keys 命令,有三个通配符:*
,?
,[]
*
- 通配任意多个字符
查询所有的键:
keys *
模糊查询 k 开头,后面随便多少个字符:
keys k*
模糊查询 e 为最后一位,前面随便多少个字符:
keys *e
双 *
模式,匹配任意多个字符 - 查询包含 k 的键:
keys *k*
?
- 通配单个字符
模糊查询 k 字头,并且匹配一个字符:
keys k?
只记得第一个字母是 k,长度是 3
keys k??
[] - 通配括号内的某一个字符
记得其他字母,第二个字母可能是 a 或 e
keys r[ae]dis
键
exists key
- 判断某个 key 是否存在
127.0.0.1:6379[15]> EXISTS k1
(integer) 1
127.0.0.1:6379[15]> EXISTS y1
(integer) 0
move key db
- 移动(剪切,粘贴)键到几号库
127.0.0.1:6379[15]> EXISTS x1 # 将 x1 移动到 8 号库
(integer) 1 # 移动成功
127.0.0.1:6379[15]> MOVE x1 8 # 查看当前库中是否存在 x1
(integer) 1 # 不存在(因为已经移走了)
127.0.0.1:6379[15]> EXISTS x1 # 切换 8 号库
(integer) 0
127.0.0.1:6379[15]> SELECT 8 # 查看当前库中的所有键
OK
127.0.0.1:6379[8]> KEYS *
1) "x1"
ttl key
- 查看键还有多久过期(-1
永不过期,-2
已过期);Time To Live
127.0.0.1:6379[8]> TTL x1
(integer) -1 # 永不过期
expire key 秒
- 为键设置过期时间(生命倒计时)
127.0.0.1:6379[8]> set k1 v1 # 保存 k1
OK
127.0.0.1:6379[8]> TTL k1 # 查看 k1 的过期时间
(integer) -1 # 永不过期
127.0.0.1:6379[8]> EXPIRE k1 10 # 设置 k1 的过期时间为 10 秒
(integer) 1 # 设置成功
127.0.0.1:6379[8]> get k1 # 获取 k1
"v1"
127.0.0.1:6379[8]> TTL k1 # 查看 k1 的过期时间
(integer) 1 # 还有 1 秒过期
127.0.0.1:6379[8]> get k1
(nil) # 从内存中销毁了
type key
- 查看键的数据类型
127.0.0.1:6379[8]> type k1
string # k1 的数据类型是 string 字符串
使用 Redis
五大数据类型
操作文档:http://redisdoc.com/
字符串 String
set / get / del / append / strlen
127.0.0.1:6379[8]> set k1 v1 # 保存数据
OK
127.0.0.1:6379[8]> set k2 v2 # 保存数据
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
127.0.0.1:6379[8]> del k2 # 删除数据 k2
(integer) 1
127.0.0.1:6379[8]> keys *
1) "k1"
127.0.0.1:6379[8]> get k1 # 获取数据 k1
"v1"
127.0.0.1:6379[8]> append k1 abc # 往 k1 的值追加数据 abc
(integer) 5 # 返回值的长度(字符数量)
127.0.0.1:6379[8]> get k1
"v1abc"
127.0.0.1:6379[8]> strlen k1 # 返回 k1 值的长度(字符数量)
(integer) 5
incr / decr / incrby / decrby
加减操作,操作的必须是数字类型
incr - increment
decr - decrement
127.0.0.1:6379[8]> set k1 1 # 初始化 k1 的值为 1
OK
127.0.0.1:6379[8]> incr k1 # k1 自增 1(相当于 ++)
(integer) 2
127.0.0.1:6379[8]> incr k1
(integer) 3
127.0.0.1:6379[8]> get k1
"3"
127.0.0.1:6379[8]> decr k1 # k1 自减 1(相当于 --)
(integer) 2
127.0.0.1:6379[8]> decr k1
(integer) 1
127.0.0.1:6379[8]> get k1
"1"
127.0.0.1:6379[8]> INCRBY k1 3 # k1 自增 3(相当于 +=3)
(integer) 4
127.0.0.1:6379[8]> GET k1
"4"
127.0.0.1:6379[8]> DECRBY k1 2 # k1 自减 2(相当于 -=2)
(integer) 2
127.0.0.1:6379[8]> get k1
"2"
getrange / setrange
类似 between … and …
range:范围
# 初始化 k1 的值为 abcdef
127.0.0.1:6379[8]> SET k1 abcdef
OK
127.0.0.1:6379[8]> get k1
"abcdef"
# 查询 k1 全部的值
127.0.0.1:6379[8]> getrange k1 0 -1
"abcdef"
# 查询 k1 的值,范围是下标 0 ~ 下标 3(包含 0 和 3,共返回 4 个字符)
127.0.0.1:6379[8]> getrange k1 0 3
"abcd"
# 替换 k1 的值,从下标 1 开始提供为 xxx
127.0.0.1:6379[8]> setrange k1 1 xxx
(integer) 6
127.0.0.1:6379[8]> get k1
"axxxef"
setex / setnx
set with expir
:添加数据的同时设置生命周期
# 添加 k1 v1 数据的同时,设置 5 秒的声明周期
127.0.0.1:6379[8]> SETEX k1 5 v1
OK
127.0.0.1:6379[8]> get k1
"v1"
# 已过期,k1 的值 v1 自动销毁
127.0.0.1:6379[8]> get k1
(nil)
set if not exist
:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉
# k1 不存在,添加成功
127.0.0.1:6379[8]> setnx k1 v1
(integer) 1
# 添加失败,因为 k1 已经存在
127.0.0.1:6379[8]> setnx k1 renda
(integer) 0
mset / mget / msetnx
m
:更多
# set 不支持一次添加多条数据
127.0.0.1:6379[8]> set k1 v1 k2 v2
(error) ERR syntax error
# mset 可以一次添加多条数据
127.0.0.1:6379[8]> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
3) "k3"
# 一次获取多条数据
127.0.0.1:6379[8]> mget k2 k3
1) "v2"
2) "v3"
# 一次添加多条数据时,如果添加的数据中有已经存在的,则失败
127.0.0.1:6379[8]> msetnx k3 v3 k4 v4
(integer) 0
# 一次添加多条数据时,如果添加的数据中都不存在的,则成功
127.0.0.1:6379[8]> msetnx k4 v4 k5 v5
(integer) 1
getset
:先 get 后 set
# 因为没有 k6,所以 get 为 null,然后将 k6 的值 v6 添加到数据库
127.0.0.1:6379[8]> getset k6 v6
(nil)
127.0.0.1:6379[8]> keys *
1) "k6"
2) "k2"
3) "k1"
4) "k4"
5) "k5"
6) "k3"
127.0.0.1:6379[8]> get k6
"v6"
# 先获取 k6 的值,然后修改 k6 的值为 vv6
127.0.0.1:6379[8]> getset k6 vv6
"v6"
127.0.0.1:6379[8]> get k6
"vv6"
列表 List
lpush / rpush / lrange
l
:left 自左向右添加 (从上往下添加)
r
:right 自右向左添加(从下往上添加)
# 从上往下添加
127.0.0.1:6379[1]> lpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> keys *
1) "list01"
# 查询 list01 中的全部数据 0 表示开始,-1 表示结尾
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# 从下往上添加
127.0.0.1:6379[1]> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
lpop / rpop
移除第一个元素(上左下右)
# 从左(上)边移除第一个元素
127.0.0.1:6379[1]> LPOP list02
"1"
# 从右(下)边移除第一个元素
127.0.0.1:6379[1]> RPOP list02
"5"
lindex
根据下标查询元素(从左向右,自上而下)
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# 从上到下数,下标为 2 的值
127.0.0.1:6379[1]> lindex list01 2
"3"
# 从上到下数,下标为 1 的值
127.0.0.1:6379[1]> lindex list01 1
"4"
llen
127.0.0.1:6379[1]> llen list01
(integer) 5
lrem
删除 n 个 value
127.0.0.1:6379[1]> lpush list01 1 2 2 3 3 3 4 4 4 4
(integer) 10
# 从 list01 中移除 2 个 3
127.0.0.1:6379[1]> lrem list01 2 3
(integer) 2
127.0.0.1:6379[1]> lrange list01 0 -1
1) "4"
2) "4"
3) "4"
4) "4"
5) "3"
6) "2"
7) "2"
8) "1"
ltrim
截取指定范围的值,别的全扔掉
ltrim key begindex endindex
127.0.0.1:6379[1]> lpush list01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379[1