NoSQL 概述

  1. 单机 MySQL 年代

    1. 数据量太大
    2. 数据索引太大
    3. 访问量(读写混合)过高
    
  2. Memcached(缓存)+ MySQL + 垂直拆分(读写分离)

发展过程:优化数据结构和索引 -> 文件缓存 -> Memcached

解决 读 的操作压力

  1. 分库分表 + 水平拆分(集群)

解决 写 的操作压力

  1. 现在时代

数据量太大,关系型数据库不够用

用户自己产生的信息,地理位置,博客,视频等

什么是NoSQL

关系型数据库:表格,行,列

非关系型数据库:一些数据不需要固定格式,不需要多余的操作就可以横向扩展

特点:

  1. 方便扩展,数据之间没有关系
  2. 大数据量,高性能(Redis 一秒写8万次,读11万次,是一种细粒度缓存,性能比较高)
  3. 数据类型多样(不需要事先设计数据库)
  4. 传统 RDBMS 和 NoSQL
传统的 RDBMS
    - 结构化组织
  - SQL
  - 数据和关系都存在单独的表
  - 操作,数据定义语言
  - 严格的一致性
  - 基础的事务
  - ......
NoSQL
    - 不仅仅是数据
  - 没有固定的查询语言
  - 键值对存储,列存储,文档存储,图形数据库
  - 最终一致性
  - CAP 定理和 BASE(异地多活)初级架构师!
  - 高性能,高可用,高可扩
  - ......

(大数据时代)了解 3V + 3高:

3V 主要描述问题:

  • 海量
  • 多样
  • 实时

3高 主要描述对程序问题

  • 高并发
  • 高可扩
  • 高性能

阿里巴巴演进分析

推荐书籍:(王坚:阿里云的这群疯子)

如果未来当一个架构师:没有什么是加一层解决不了的

# 1. 商品基本信息
        名称、价格、商家信息
    关系型数据库:MySQL / Oracle (淘宝早年就去IOE(在阿里巴巴的IT架构中,去掉IBM的小型机、Oracle数据库、EMC存储设备,代之以自己在开源软件基础上开发的系统)!)
    淘宝内部的 MySQL 不是大家用的 MySQL,他根据自己的需求做了相应修改
    
# 2. 商品描述、评论(文字比较多)
        文档数据库 MangoDB
    
# 3. 图片
        分布式文件系统 FastDFS
    - 淘宝自己的 TFS
    - Google GFS
    - Hadoop HDFS
    - 阿里云 OSS
    
# 4. 商品关键词(搜索)
        - 搜索引擎 solr elasticsearch
    - ISerach 淘宝 多隆
    
# 5. 商品热门的波段信息
        - 内存数据库
        - redis tair memache
    
# 6. 商品的交易,外部的支付接口
        - 三方应用

大型互联网应用问题

  • 数据类型太多
  • 数据源多,经常重构
  • 数据要改造,大面积改造

解决方案:UDSL(统一数据服务平台)

NoSQL 四大分类

键值对

  • 新浪:redis
  • 美团:redis + Tair
  • 阿里、百度:Redis + memecache

文档数据库

  • MongoDB

    • 是一个基于分布式文件存储的数据库,c++编写,主要用来处理大量文档!
    • 是一个介于关系型数据库和非关系型数据库之间的中间产品,MongoDB是关系型数据库中功能最丰富,最像关系型数据库的

图关系数据库

  • 放的是关系,如:朋友圈社交网络,广告推荐等

列存储数据库

  • Hbase
  • 分布式文件系统

Redis 入门

什么是 Redis

远程字典服务(Remote Dictionary Server)

是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型

能干什么

内存存储、持久化、内存中断电即失,所以持久化很重要(RDB、AOF)

发布订阅系统

地图信息分析

计数器,计时器

特性

多样化数据类型

持久化

集群

官网:http://www.redis.cn/

Docker 安装

  1. 创建外部挂载目录:
mkdir -p /root/docker/redis/data
mkdir -p /root/docker/redis/conf
  1. 下载 redis.conf 配置文件到 conf 目录下,后给文件授权
wget http://download.redis.io/redis-stable/redis.conf
chmod 777 redis.conf
  1. 修改配置信息
bind 127.0.0.1 通过#注释掉,解除本地连接限制
protected-mode yes 默认no,保护模式,限制为本地访问,修改后解除保护模式
daemonize yes 注释掉
appendonly yes 持久化
requirepass 123456 密码
  1. 启动命令
docker run -p 6379:6379 -v /root/docker/redis/data:/data -v /root/docker/redis/conf/redis.conf:/etc/redis/redis.conf --name myredis -d redis redis-server /etc/redis/redis.conf --appendonly yes --requirepass 123456

// 从机配置命令
docker run -p 6380:6379 -v /root/docker/redis/data81:/data -v /root/docker/redis/conf/redis80.conf:/etc/redis/redis.conf --name redis80 -d redis redis-server /etc/redis/redis.conf --appendonly yes --requirepass 123456

# 命令解析
    -p 6379:6379    端口映射
  --name myredis 指定容器名称
  -d redis 后台启动
  --appendonly yes 开启持久化
  --requirepass 123456 设置密码
  -v /root/docker/redis/data:/data 
  -v /root/docker/redis/conf/redis.conf:/etc/redis/redis.conf --name myredis  数据卷映射

进入容器

# docker exec -it myredis bash
# redis-cli
127.0.0.1>auth 123456   // 认证用户,不然无法使用

Redis-benchmark 性能测试工具

# 命令
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
# 运行结果
====== SET ======
  100000 requests completed in 3.12 seconds  // 10万个请求在3.12秒内完成
  100 parallel clients  // 100 个并发客户端
  3 bytes payload  // 每次写入3个字节
  keep alive: 1  // 只有一台服务器来处理这些请求,单机性能
  multi-thread: no

0.09% <= 1 milliseconds
99.54% <= 2 milliseconds
99.86% <= 3 milliseconds
99.96% <= 4 milliseconds
100.00% <= 4 milliseconds
32061.56 requests per second  // 每秒处理3万2千请求

基础知识

默认有16个数据库 在配置文件中 database 关键字中配置

  1. 切换数据库
127.0.0.1:6379> select 3   // 切换数据库
OK
127.0.0.1:6379[3]>DBSIZE   // 返回当前数据库内 keys 数量
(integer) 0
127.0.0.1:6379[3]> set name 'xiong' // 添加键值
OK
127.0.0.1:6379[3]> get name  // 根据 key 查询 value
"xiong"
127.0.0.1:6379[3]> DBSIZE
(integer) 1
127.0.0.1:6379[3]> keys *  // 查看所有键
1) "name"
127.0.0.1:6379[3]>flushdb  // 清空本数据库
127.0.0.1:6379[3]>flushall // 清空所有数据库
  1. redis 是单线程的

redis 基于内存操作,CPU 不是 redis 性能瓶颈,reids 的瓶颈为机器内存和网络带宽

redis 为单线程还这么快?

误区1:高性能服务器一定是多线程

误区2:多线程(CPU上下文切换)一定比单线程效率高

CPU > 内存 > 硬盘

核心:redis 是将所有的数据放在内存中,所以说使用单线程操作是效率最高的,多线程(CPU上下文切换)耗时间,对于内存来说没有上下文切换效率就是最高的!多次读写都是在一个CPU上

五大基本数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

redis-key

基础命令:http://www.redis.cn/commands.html

判断是否存在exists name
移除一个键move name 1 (1 代表当前数据库)
设置过期时间expire name 10 (设置为10s 过期)
查看当前数据类型type name

String(字符串)

追加字符append key ‘hello’ (如果key不存在,则新建)
获取数据长度strlen key 查看数据长度
自增1incr view
自减1decr view
setex // 设置过期时间setex name 30 ‘xiong’ (30 秒后过期)
setnx // 不存在再设置setnx name ‘xiong’ (如果存在,不设置,不存在就设置)
mset // 设置多个值mset k1 v1 k2 v2
mget // 获取多个值mget k1 k2
getsetgetset name redis # 如果不存在值,则返回 nil;如果存在,则返回原来的值再修改
设置对象set user:1:{name:zhang,age:3} // 设置一个user:1 对象值为 json 字符串mset user:1:name zhang user:1:age:3 // 批量设置的方法

使用场景:

  • 计数器 (博客浏览量等)
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储!

List(列表)

基本数据类型:列表

在 Redis 内可以实现:栈、队列

127.0.0.1:6379> lpush list one  // 向列表内插入值(头部插入,类似栈)
(integer) 1
127.0.0.1:6379> lpush list two 
(integer) 2
127.0.0.1:6379> lrange list 0 -1  // 获取指定范围内的值
1) "two"
2) "one"
127.0.0.1:6379> rpush list three  // 向右插入(尾部插入)
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "three"
==================================================
pop # 移除操作 
127.0.0.1:6379> lpop list   // 移除头部的值
"two"
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "three"
127.0.0.1:6379> rpop list   // 移除尾部的值
"three"
127.0.0.1:6379> lrange list 0 -1
1) "one"
===================================================
lindex # 获取下标某个值
127.0.0.1:6379> lindex list 1
"two"
==================================================
# 获取长度 llen
127.0.0.1:6379> llen list
(integer) 3
==================================================
lrem # 移除指定个数的 value 
127.0.0.1:6379> lrem list 1 one  # lrem key count value
(integer) 1
==================================================
ltrim # 截取范围内的值 
127.0.0.1:6379> rpush mylist 'hello'
(integer) 1
127.0.0.1:6379> rpush mylist 'hello1'
(integer) 2
127.0.0.1:6379> rpush mylist 'hello2'
(integer) 3
127.0.0.1:6379> rpush mylist 'hello3'
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 1 2    # ltrim mylist start end
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
======================================================
rpoplpush # 移除最后一个元素并保存到其他列表  
127.0.0.1:6379> rpush list 'hello' 'hello1' 'hello2' 'hello3'
(integer) 4
127.0.0.1:6379> rpoplpush list otherlist
"hello3"
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379> lrange otherlist 0 -1
1) "hello3"
========================================================
lset # 修改列表内的值 (不存在的值会报错)
127.0.0.1:6379> rpush list 'hello'
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "hello"
127.0.0.1:6379> lset list 0 'item'  # lset key index element
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
=========================================================
linsert # 插入一个值,定位为列表中的 value
127.0.0.1:6379> linsert list before item 'new'  # 向前插入
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "new"
2) "item"
127.0.0.1:6379> linsert list after item 'new' # 向后插入
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "new"
2) "item"
3) "new"

小结

  • 实际上是一个链表,before Node after,left right 都可以插入值
  • 如果key 不存在,创建新的链表
  • 如果key 存在,新增内容
  • 如果移除所有值,空链表,也代表不存在
  • 两边插入或者改动值,效率最高!中间元素效率相对较低

Set(集合)

set 中的值不能重复

127.0.0.1:6379> sadd myset 'hello' 'xiong' 'love'   # 向集合中添加值
(integer) 3
127.0.0.1:6379> SMEMBERS set
(empty array)
127.0.0.1:6379> SMEMBERS myset      # 查看集合中所有值
1) "xiong"
2) "hello"
3) "love"
127.0.0.1:6379> SISMEMBER myset xiong  # 判断一个值是否在集合中
(integer) 1
127.0.0.1:6379> SISMEMBER myset love
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0
========================================================
scard # 获取集合中元素个数
127.0.0.1:6379> scard myset
(integer) 3
========================================================
srem # 移除某个值
127.0.0.1:6379> srem myset xiong
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "love"
========================================================
SRANDMEMBER # 随机抽选一个值
127.0.0.1:6379> SRANDMEMBER myset    # SRANDMEMBER set count
"love"
127.0.0.1:6379> SRANDMEMBER myset
"hello"
========================================================
spop # 随机删除一个元素
127.0.0.1:6379> spop myset # spop set count
"love"
========================================================
# sdiff # 求取差集
========================================================
sinter # 求取交集
========================================================
sunion # 求取并集

用途:

  • 微博,B站的 共同关注 (交集)

Hash(哈希)

Map 集合,key-Map ,本质和 string 没有区别

127.0.0.1:6379> hset myhash filed1 xionog       # 存值
(integer) 1
127.0.0.1:6379> hget myhash filed1      # 取值
"xionog"
========================================================
取值
hmget   # 获取多个
hgetall # 获取全部
========================================================
删除
hdel
========================================================
hlen # 获取 hash 表的字段数量
========================================================
hexists # 判断 hash 表的字段是否存在
========================================================
hkeys # 获取 hash 表的所有值

应用

  • 更适合对象的存储

Zset(有序集合)

在 set 的基础上,增加了一个值

127.0.0.1:6379> zadd myset 1 one    # 添加一个值 zadd key score value
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 1
127.0.0.1:6379> zadd myset 3 three
(integer) 1
127.0.0.1:6379> ZRANGE myset 0 -1  # 查看值
1) "one"
2) "two"
3) "three"
========================================================
ZRANGEBYSCORE # 排序
127.0.0.1:6379> zadd saraly 300 xiong 200 xin 400 qiang
(integer) 3
127.0.0.1:6379> zrange saraly 0 -1
1) "xin"
2) "xiong"
3) "qiang"
127.0.0.1:6379> ZRANGEBYSCORE saraly -inf +inf withscores  # 排序,只能从小到大,在设置值的时候就已经排好序了
1) "xin"
2) "200"
3) "xiong"
4) "300"
5) "qiang"
6) "400"
127.0.0.1:6379> zrevrange saraly 0 -1  # 排序从大到小
1) "qiang"
2) "xiong"
3) "xin"
========================================================
zrem # 移除元素
========================================================
zcard # 获取集合中元素个数
========================================================
zcount # 获取指定区间内的元素个数

应用

  • 存储班级程序表
  • 普通消息和重要消息区分,按权重进行判断
  • 排行榜实现

三种特殊数据类型

geospatial 地理位置

朋友的定位,附近的人,打车距离计算

redis 的 Geo 在 Redis 3.2 版本就推出了,可以推算地理位置信息

测试数据连接:http://www.jsons.cn/lngcode/

相关命令:http://www.redis.cn/commands/geoadd.html

  • GEOADD
  • GEODIST
  • GEOHASH
  • GEOPOS
  • GEORADIUS
  • GEORADIUSBYMEMBER
geoadd # 添加地理位置 (两级-南极北极,无法直接添加)
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijin
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shengzheng
(integer) 2
========================================================
由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:
有效的经度从-180度到180度
有效的纬度从-85.05112878度到85.05112878度
========================================================
GEODIST key member1 member2
返回两个给定位置之间的距离。
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
    - m 表示单位为米。
    - km 表示单位为千米。
    - mi 表示单位为英里。
    - ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
127.0.0.1:6379> geodist china:city shanghai beijin
"1067378.7564"
========================================================
GEOPOS key member [member ...]
从key里返回所有给定位置元素的位置(经度和纬度)。
给定一个sorted set表示的空间索引,密集使用 geoadd 命令,它以获得指定成员的坐标往往是有益的。当空间索引填充通过 geoadd 的坐标转换成一个52位Geohash,
所以返回的坐标可能不完全以添加元素的,但小的错误可能会出台。
因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。
========================================================
GEORADIUS key longitude latitude radius m|km|ft|mi
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

案例,获取附近的人:
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "chongqing"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord  # 查询他人的经纬度
1) 1) "chongqing"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist # 查询显示到中心距离的位置
1) 1) "chongqing"
   2) "341.9374"
========================================================
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心

127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 1000 km
1) "shanghai"
========================================================
GEOHASH key member [member ...]
该命令返回11个字符的字符串
127.0.0.1:6379> geohash china:city shanghai chongqing
1) "wtw3sj5zbj0"
2) "wm5xzrybty0"
# 将二维经纬度转换为一维字符串,如果两个字符串越接近,则距离越近

GEO 底层为 zset 集合所以可以用 zset 命令

hyperloglogs

什么是基数:

一个集合中不重复元素的个数

优点:占用的内存是固定的,2^64 不同的元素基数,只需要废12kb内存!如果要从内存角度来比骄傲的话 hyperloglog 首选

应用:网页的 UV (一个人访问一个网站多次,访问量为1)

传统方式:使用 set 集合储存用户id,然后统计 set 集合中的元素个数作为标准判断

127.0.0.1:6379> pfadd mykey a b c d e f g h i j  # 添加元素
(integer) 1
127.0.0.1:6379> pfcount mykey       # 统计基数个数
(integer) 10
127.0.0.1:6379> pfadd mykey2 i j a l n m e c x
(integer) 1
127.0.0.1:6379> pfmerge mykey3 mykey mykey2  # 合并两个集合,并集
OK
127.0.0.1:6379> pfcount mykey3
(integer) 14

如果允许容错,则使用 hyperloglog,出错率为 0.81%

Bitmaps

位存储(只有 0 和 1)都是操作二进制为来进行记录,就只有0和1两个状态

应用:

统计用户信息:活跃,不活跃;登陆,未登录;打卡

127.0.0.1:6379> setbit sign 0 1  # 存值
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 0
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> getbit sign 3       # 取值
(integer) 0
127.0.0.1:6379> bitcount sign   # 统计
(integer) 2

事务

Redis 单条命令保证原子性,但是事务不保证原子性!

本质: 一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,按顺序执行

所有命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!

Redis 事务

  • 开启事务
  • 命令入队
  • 执行事务
127.0.0.1:6379> multi               # 开启事务
OK
127.0.0.1:6379> set k1 v1       # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec                # 执行事务
1) OK
2) OK
3) OK
4) "v2"
=============================================================
127.0.0.1:6379> multi               # 开启事务
OK
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard         # 取消事务
OK
127.0.0.1:6379> get k4          # 事务队列中的命令不会执行
(nil)
127.0.0.1:6379>

编译型异常

代码有问题,命令有错误

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379> get k2
(nil)

命令都不会运行,整个事务都取消

运行时异常

语法型问题,其他命令正常执行,错误出抛出异常

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) OK

监控

悲观锁

  • 很悲观,什么时候都会出问题,无论做什么都加锁

乐观锁

  • 很乐观,什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取 version
  • 更新的时候比较 version

Redis 监控测试

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money             # 监视 money 对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec        # 正常执行  成功后监控会自动取消
1) (integer) 80
2) (integer) 20
=============================================================
# 测试多线程修改值,使用 watch 可以当作 reids 的乐观锁操作
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
                # 另一主机连接并修改监控的值后
        127.0.0.1:6379> get money
        "80"
        127.0.0.1:6379> set money 1000
        OK
127.0.0.1:6379> exec        # 监视失败,事务取消
(nil)

失败后的步骤

  1. 放弃监视 unwatch
  2. 重新监视 watch key
  3. 事务执行是比对监视的值是否变化,如果没有变化则执行,如果有变化则放弃监视,重新监视执行

Jedis

是官方推荐的 java 连接开发工具!使用 java 操作的Redis 中间件!

构建项目:

创建一个空项目:

构建 maven 项目 -> 修改:

image.pngimage.png

导入对应依赖

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.68</version>
</dependency>

测试连接

public class TestPing {
    public static void main(String[] args) {
        // 1. new jedis 对象
        Jedis jedis = new Jedis("ip",6379);
        // 认证
        jedis.auth("123456");
        // 测试连接
        System.out.println(jedis.ping());
        jedis.close(); // 关闭连接
    }
}

常用Api

与常用基础类型的命令基本同,特殊类型也相同,只不过换成了方法

事务

public class TestPing {
    public static void main(String[] args) {
        // 1. new jedis 对象
        Jedis jedis = new Jedis("159.75.113.81",6379);
        // 认证
        jedis.auth("123456");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","xiong");
        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();

        try {
            multi.set("user1", result);
            multi.set("user2", result);

            multi.exec();  // 执行事务
        } catch (Exception e) {
            multi.discard();  // 出异常放弃事务
        } finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();  // 关闭连接
        }
    }
}

输出:
{"name":"xiong","hello":"world"}
{"name":"xiong","hello":"world"}

redis.conf 详情

配置文件对大小写不敏感

网络配置:

bind 127.0.0.1      # 绑定 ip
protected-mode no       # 保护模式
port 6379                       # 端口号

通用配置:

# 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  # 以守护进程方式运行,默认是 no 

pidfile /var/run/redis_6379.pid # 如果以后台方式运行,我们就需要指定一个 pid 文件

日志级别:
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice

logfile ""  # 日志保存文件名,如果为空,则为输出

databases 16 # 数据库数量
======================================================================================
快照  (持久化使用)
    在规定时间内,执行了多少次操作,则会持久化到文件 rdb aof
  如果没有持久化,则 redis 断电会丢失数据
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behavior will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""
save 900 1 # 如果900 秒内有一个 key 值改变就进行持久化操作
save 300 10 # 如果300 秒内有 10 个 key 值改变就进行持久化操作
save 60 10000 # 如果 60 秒内,有 10000 个 key 值改变就进行持久化操作

stop-writes-on-bgsave-error yes # 持久化出错,是否继续工作

rdbchecksum yes     # 保存压缩rdb 文件的时候,进行错误校验

rdbcompression yes # 是否压缩 rdb 文件,需要消耗 cpu 资源

dir ./ # rdb 文件保存路径

主从复制:

安全:

requirepass 123456      # 设置密码,默认没有
=========================================
通过命令设置密码:
127.0.0.1:6379> config set requirepass "123456"
获取密码
127.0.0.1:6379> config get requirepass

客户端限制:

maxclients 10000        # 设置能连接的redis 最大客户端数量
maxmemory <bytes>       # redis 配置最大的内存

# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
    # volatile-lfu -> Evict using approximated LFU, only keys with an expire set.       只对设置了过时间的key 进行lru(默认值)
    # allkeys-lfu -> Evict any key using approximated LFU.                                                  删除lru 算法的key
    # volatile-random -> Remove a random key having an expire set.                                  随机删除即将过期的key
    # allkeys-random -> Remove a random key, any key.                                                               随机删除key
    # volatile-ttl -> Remove the key with the nearest expire time (minor TTL)               删除即将过期的key
    # noeviction -> Don't evict anything, just return an error on write operations. 永不过期,报错
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, Redis will return an error on write
#       operations, when there are no suitable keys for eviction.
#
#       At the date of writing these commands are: set setnx setex append
#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
#       getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction  # 内存满后的策略

AOF 配置:

appendonly yes  # 默认为 no,默认使用 rdb 方式持久化,在大部分情况下, rdb 完全够用

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof" # 文件名
自动持久化方式:
# appendfsync always        # 每次修改都追加同步,消耗性能
appendfsync everysec        # 每秒一次
# appendfsync no                # 系统自动同步数据,不追加同步,速度最快

Redis 持久化

RDB

持久化操作流程图:

img

rdb 保存文件为 dump.rdb(在bin 文件夹中)该文件是一个压缩过的二进制文件,可以通过该文件还原快照时的数据库状态,即生成该RDB文件时的服务器数据

有时候在生产环境中会进行备份

触发规则:

  1. save 规则(配置文件中写的)情况下自动触发保存
  2. 执行savebgsave命令

执行savebgsave命令,可以手动触发快照,生成RDB文件,两者的区别如下

使用save命令会阻塞Redis服务器进程,服务器进程在RDB文件创建完成之前是不能处理任何的命令请求

127.0.0.1:6379> save
OK
复制代码

而使用bgsave命令不同的是,bgsave命令会fork一个子进程,然后该子进程会负责创建RDB文件,而服务器进程会继续处理命令请求

127.0.0.1:6379> bgsave
Background saving started
  1. 执行 flushall 命令,也会触发保存
  2. 退出 redis,也会触发保存

恢复:

  1. 只需要将 rdb 文件放在 redis 启动目录就可以,redis 启动时会自动检查 dump.rdb 并恢复其中数据!
  2. 查看需要存在的位置 config get dir

优点

  • RDB快照是一个压缩过的非常紧凑的文件,保存着某个时间点的数据集,适合做数据的备份,灾难恢复
  • 可以最大化Redis的性能,在保存RDB文件,服务器进程只需fork一个子进程来完成RDB文件的创建,父进程不需要做IO操作
  • 与AOF相比,恢复大数据集的时候会更快

缺点

  • RDB的数据安全性是不如AOF的,保存整个数据集的过程是比繁重的,根据配置可能要几分钟才快照一次,如果服务器宕机,那么就可能丢失几分钟的数据
  • Redis数据集较大时,fork的子进程要完成快照会比较耗CPU、耗时

AOF

将我们的所有命令都记录下来(在大量数据时效率很慢)

开启:将 appendonly 改为 yes

如果 AOF 文件超过 64mb ,会 fork 一个新的进程来将我们的文件进行重写

image.png

触发保存规则时会将所写入的命令记录进 appendonly.aof 文件中

如果 aof 文件有错误,redis 就无法启动

可以通过 redis-check-aof --fix appendonly.aof 进行修复(通过删除错误命令进行修复)

优点:

  • 数据更完整,安全性更高,秒级数据丢失(取决fsync策略,如果是everysec,最多丢失1秒的数据)
  • AOF文件是一个只进行追加的日志文件,且写入操作是以Redis协议的格式保存的,内容是可读的,适合误删紧急恢复

缺点:

  • 对于相同的数据集,AOF文件的体积要大于RDB文件,数据恢复也会比较慢
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。 不过在一般情况下, 每秒 fsync 的性能依然非常高

总结

  • 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化
  • 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB
  • 如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失

Redis 订阅发布

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

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

消息发布图

img

测试:

127.0.0.1:6379> subscribe xiongxinq     // 订阅频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "xiongxinq"
3) (integer) 1
        # 第二个客户端
    127.0.0.1:6379> publish xiongxinq "hello wirld"     // 向指定频道发送消息
    (integer) 1
    127.0.0.1:6379>
1) "message"
2) "xiongxinq"
3) "hello wirld"

命令

1[PSUBSCRIBE pattern pattern …] 订阅一个或多个符合给定模式的频道。
2[PUBSUB subcommand argument [argument …]] 查看订阅与发布系统状态。
3PUBLISH channel message 将信息发送到指定的频道。
4[PUNSUBSCRIBE pattern [pattern …]] 退订所有给定模式的频道。
5[SUBSCRIBE channel channel …] 订阅给定的一个或多个频道的信息。
6[UNSUBSCRIBE channel [channel …]] 指退订给定的频道。

原理 Redis 使用 c 实现的们可以通过 pubsub.c 文件了解

使用场景:

  • 实时消息系统
  • 实时聊天(聊天室)
  • 订阅、关注系统

稍微复杂的场景会使用消息中间件

主从复制

概述

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

作用:

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
127.0.0.1:6379> info replication        # 查看当前库信息
# Replication
role:master
connected_slaves:0      # 从机数量
master_replid:11303f849410c9f84ea7b569a2a678ba898c1ab5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>

环境配置

伪集群配置(Docker 环境下)

步骤:

  1. 创建挂载目录:mkdir /root/docker/redis/data80
  2. 复制配置文件:cp /root/docker/redis/conf/redis.conf /root/docker/redis/conf/redis80.conf(别忘了赋予执行权限)
  3. docker 运行,将data挂载在80端口,并修改容器名称,参考Docker 安装章节

从机配置

两种方式

config set masterauth 123456
slaveof 服务器ip 6379
=====================================================================
配置好后主机信息:(我这里加了两台redis容器)
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=159.75.113.62,port=6379,state=online,offset=266,lag=1
slave1:ip=159.75.113.62,port=6379,state=online,offset=266,lag=0
master_replid:60f275d03684ec3aaff952e00c795a93e24fd305
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:266
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:266
=====================================================================
从机信息
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:159.75.113.62
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:60f275d03684ec3aaff952e00c795a93e24fd305
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
========================================================================
从节点无法写入
127.0.0.1:6379> set key1 "xiong"
(error) READONLY You can't write against a read only replica.

真实的主从配置是在配置文件中配置的,用命令配置的只是暂时的。

  • 主机宕机,从机还是从机,无法变成主机要改变此情况需要配置哨兵

可以使用slaveof no one让自己变成主机,其他的节点就可以手动来连接到新的主节点,当原先主机恢复时,主从关系需要从新配置。

  • 从机宕机,主机中的从机数量减一,从机恢复时会将主机在宕机阶段的数据重新备份

复制原理

在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;

在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。

  • 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  • 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

哨兵模式

Redis 的 主从复制 模式下,一旦 主节点 由于故障不能提供服务,需要手动将 从节点 晋升为 主节点,同时还要通知 客户端 更新 主节点地址,这种故障处理方式从一定程度上是无法接受的。Redis 2.8 以后提供了 Redis Sentinel 哨兵机制 来解决这个问题。

Redis Sentinel 是 Redis 高可用 的实现方案。Sentinel 是一个管理多个 Redis 实例的工具,它可以实现对 Redis 的 监控通知自动故障转移

16560ce61dbc4eeb.png

切换过程:

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

配置文件 sentinel.conf

# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义
192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.11.128 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456

修改完后可以进入 src 目录通过 ./redis-sentinel ../sentinel.conf启动哨兵

其他配置选项

配置项参数类型作用
port整数启动哨兵进程端口
dir文件夹目录哨兵进程服务临时文件夹,默认为/tmp,要保证有可写入的权限
sentinel down-after-milliseconds<服务名称><毫秒数(整数)>指定哨兵在监控Redis服务时,当Redis服务在一个默认毫秒数内都无法回答时,单个哨兵认为的主观下线时间,默认为30000(30秒)
sentinel parallel-syncs<服务名称><服务器数(整数)>指定可以有多少个Redis服务同步新的主机,一般而言,这个数字越小同步时间越长,而越大,则对网络资源要求越高
sentinel failover-timeout<服务名称><毫秒数(整数)>指定故障切换允许的毫秒数,超过这个时间,就认为故障切换失败,默认为3分钟
sentinel notification-script<服务名称><脚本路径>指定sentinel检测到该监控的redis实例指向的实例异常时,调用的报警脚本。该配置项可选,比较常用

缓存穿透和雪崩

数据请求过程(有缓存的情况下):

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

缓存穿透

img

如果缓存中没有,数据库中也没有,数据不会更新缓存,但如果有大量请求(抢购秒杀时!)想要获取这个数据,涌入数据库,导致数据库崩溃。

解决方案:

增加校验,“加一层”,对没有的数据请求进行拦截(如 id = -1)

布隆过滤器:

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。把已存在数据的key存在布隆过滤器中。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存查询数据库。

缺点:

  1. 会存在一定误判的几率
  2. 对新增加的数据无法进行布隆过滤
  3. 数据的 key 不会频繁的更改

缓存击穿

指单个热门的 key 在不停的抗大并发,大并发集中对着一个点进行访问,当这个 key 在失效(过期或者其他原因)的瞬间,继续的大并发就穿破缓存,直接请求数据库(数据库中有这个数据),就像在一个屏障上凿开一个洞一样,大量请求涌入数据库导致数据库崩溃。

解决方案

  1. 设置热点数据永远不过期
  2. 加互斥锁,保证每次只有一个请求能够到数据库查询,其余进行等待

雪崩

多个 key 出现高并发查询,缓存中失效或者查询不到,然后直接请求数据库,导致数据库压力飙升

解决方案:

  • Redis 高可用
    搭建 Redis 集群

  • 限流降级
    在缓存失效后,通过加锁或者队列来控制读数据库与缓存的线程数量。比如对某一个 key 只允许一个线程直接查询和写,其他等待

  • 数据预热
    在正式部署前,把可能的数据预先访问一遍,这样部分可能大量访问的数据会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的 key,这只不同的过期时间,让缓存失效的时间点尽量均匀。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐