NoSQL 和 Redis 概述

在日常的开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题,可是一旦涉及大数据量的需求,比如一些商品抢购的情景,或者是主页访问量瞬间较大的时候,单一使用数据库来保存数据的系统会因为面向磁盘,磁盘读/写速度比较慢的问题而存在严重的性能弊端,一瞬间成千上万的请求到来,需要系统在极短的时间内完成成千上万次的读/写操作,这个时候往往不是数据库能够承受的,极其容易造成数据库系统瘫痪,最终导致服务宕机的严重生产问题。

为了克服上述的问题,项目通常会引入 NoSQL 技术,这是一种基于内存的数据库,并且提供一定的持久化功能。

NoSQL,指的是非关系型数据库。NoSQL(Not Only SQL),是对不同于传统的关系型数据库的数据库管理系统的统称。对 NoSQL 最普遍的解释是”非关联型的”,强调 Key-Value Stores 和文档数据库的优点,而不是单纯的反对 RDBMS。

NoSQL 用于超大规模数据的存储。(例如谷歌或 Facebook 每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。

NoSQL 数据库主要有以下四类:

  • 基于键值对 key-value 类型:Redis,memcached
  • 列存储数据库 Column-oriented Graph:HBase
  • 图形数据库 Graphs based:Neo4j
  • 文档型数据库: MongoDB
    • MongoDB是一个基于分布式文件存储的数据库,主要用来处理大量的文档。

Redis 是什么?
Remote Dictionary Service(远程字典服务器)
Redis 是一个开源(BSD许可)的,C语言编写的,高性能的数据结构存储系统,它可以用作数据库缓存消息中间件。它基于内存运行并支持持久化的 NoSQL 数据库,是当前最热门的 NoSQL 数据库之一。

相关网站:

Redis 的特性:持久化、丰富的数据类型、数据备份(主从复制)。

Redis 的优点:

  • 性能极高
  • 丰富的数据类型
    • Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作
  • 原子性
  • 丰富的特性
    • Redis 支持 publish / subscribe 、通知、key 过期等特性

安装按官网走即可。

1
2
127.0.0.1:6379> ping
PONG

Redis 是一个字典结构的存储服务器,而实际上一个 Redis 实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。

每个数据库对外都是一个从 0 开始的递增数字命名,Redis 默认支持 16 个数据库(可以通过配置文件支持更多,无上限),可以通过配置 databases 来修改这一数字。

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 10
OK
127.0.0.1:6379[10]> select 15
OK
127.0.0.1:6379[15]> select 16
(error) ERR DB index is out of range
127.0.0.1:6379[15]> select 0
OK
127.0.0.1:6379>

常用命令

进入 redis :

1
redis-cli

数据库的切换:

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 10
OK
127.0.0.1:6379[10]> select 15
OK
127.0.0.1:6379[15]> select 16
(error) ERR DB index is out of range
127.0.0.1:6379[15]> select 0
OK
127.0.0.1:6379>

不言之教:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> set k2 200
OK
127.0.0.1:6379> dbsize
(integer) 2
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379> keys k?
1) "k1"
2) "k2"
127.0.0.1:6379> set k12 12
OK
127.0.0.1:6379> keys k?
1) "k1"
2) "k2"

删除 key :

1
2
3
4
5
6
7
8
127.0.0.1:6379> keys *
1) "k1"
2) "k12"
3) "k2"
127.0.0.1:6379> del k12
(integer) 1
127.0.0.1:6379> del k12
(integer) 0

清除当前数据库:

1
2
3
4
5
6
7
8
127.0.0.1:6379[1]> set k1 100
OK
127.0.0.1:6379[1]> dbsize
(integer) 1
127.0.0.1:6379[1]> flushdb
OK
127.0.0.1:6379[1]> keys *
(empty array)

清空所有数据库:

1
flushall

判断某个 key 是否存在:

1
2
3
4
5
6
7
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379> Exists k1
(integer) 1
127.0.0.1:6379> Exists k12
(integer) 0

把 key 从当前库移动到目标库:

1
2
3
4
5
6
7
8
127.0.0.1:6379> move k1 15
(integer) 1
127.0.0.1:6379> keys *
1) "k2"
127.0.0.1:6379> select 15
OK
127.0.0.1:6379[15]> keys *
1) "k1"

查看 key 的类型:

1
2
127.0.0.1:6379> type k2
string

为给定的key设置过期的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
127.0.0.1:6379> keys *
1) "k2"
127.0.0.1:6379> Expire k2 10
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 8
127.0.0.1:6379> ttl k2
(integer) 7
127.0.0.1:6379> ttl k2
(integer) 6
127.0.0.1:6379> ttl k2
(integer) 3
127.0.0.1:6379> ttl k2
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 0
127.0.0.1:6379> ttl k2
(integer) -2
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> ttl k2
(integer) -2
127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> ttl k1
(integer) -1

在上面的命令中,-2 表示数据已经消失,-1 表示该数据不会过期。

基本数据类型

65-1.png

string

string 是 redis 最基本的类型,可以理解成一个 key 对应一个 value . 一个 string 类型的值最大能存储512MB .

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。如 jpg 图片或序列化的对象。

设定指定key的值、获取指定key的值:

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set k1 10
OK
127.0.0.1:6379> set k2 20
OK
127.0.0.1:6379> set k3 hello
OK
127.0.0.1:6379> get k1
"10"
127.0.0.1:6379> get k3
"hello"

设置、获取多个给定的 key 值:

1
2
3
4
5
6
127.0.0.1:6379> mset k11 11 k12 12 k13 world
OK
127.0.0.1:6379> mget k11 k12 k13
1) "11"
2) "12"
3) "world"

二进制安全的体现(设置的是什么,获取的就是什么):

1
2
3
4
127.0.0.1:6379> set k22 hello\0world
OK
127.0.0.1:6379> get k22
"hello\\0world"

返回字符串的子串:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> get k3
"hello"
127.0.0.1:6379> GETRANGE k3 0 1
"he"
127.0.0.1:6379> GETRANGE k3 0 3
"hell"
127.0.0.1:6379> set k4 askgalfja;fegrga
OK
127.0.0.1:6379> get k4
"askgalfja;fegrga"
127.0.0.1:6379> GETRANGE k4 0 -1
"askgalfja;fegrga"

覆盖字符串的值:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> get k3
"hello"
127.0.0.1:6379> SETRANGE k3 0 wor
(integer) 5
127.0.0.1:6379> get k3
"worlo"
127.0.0.1:6379> SETRANGE k3 waipoqiaoyaoayao
(error) ERR wrong number of arguments for 'setrange' command
127.0.0.1:6379> SETRANGE k3 0 waipoqiaoyaoayao
(integer) 16
127.0.0.1:6379> get k3
"waipoqiaoyaoayao"

设新值,返回旧值:

1
2
3
4
5
6
127.0.0.1:6379> get k1
"10"
127.0.0.1:6379> GETSET k1 hello
"10"
127.0.0.1:6379> get k1
"hello"

set 值,并设置过期时间(以秒为单位):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> SETEX k1 10 200
OK
127.0.0.1:6379> get k1
"200"
127.0.0.1:6379> ttl k1
(integer) 6
127.0.0.1:6379> ttl k1
(integer) 4
127.0.0.1:6379> ttl k1
(integer) 3
127.0.0.1:6379> ttl k1
(integer) 1
127.0.0.1:6379> ttl k1
(integer) -2

加一 / 加很多(被加的必须是数值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> INCR k1
(integer) 101
127.0.0.1:6379>
127.0.0.1:6379> INCR k1
(integer) 102
127.0.0.1:6379> INCR k1
(integer) 103
127.0.0.1:6379> INCR k1
(integer) 104
127.0.0.1:6379> INCR k1
(integer) 105
127.0.0.1:6379> get k1
"105"
127.0.0.1:6379> INCRBY k1 95
(integer) 200
127.0.0.1:6379> get k1
"200"

list(双向链表)

插入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> lpush list1 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> rpush list1 7 8 9 10
(integer) 10
127.0.0.1:6379> lrange list1 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"
7) "7"
8) "8"
9) "9"
10) "10"

出队:

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> lpop list1
"6"
127.0.0.1:6379> rpop list1
"10"
127.0.0.1:6379> LRANGE list1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "7"
7) "8"
8) "9"

通过下标设置列表元素的值,下标从0开始:

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> lset list1 5 1000
OK
127.0.0.1:6379> LRANGE list1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "1000"
7) "8"
8) "9"

通过下标获取列表中的元素:

1
2
127.0.0.1:6379> LINDEX list1 5
"1000"

从队头开始移除 count 个值为 value 的列表元素:

1
LREM key count value

对一个列表进行修剪(trim),即,只保留指定区间内的元素,其余元素将被删除:

1
2
3
4
5
6
7
127.0.0.1:6379> LTRIM list1 2 5
OK
127.0.0.1:6379> LRANGE list1 0 -1
1) "3"
2) "2"
3) "1"
4) "1000"

总结: redis 中的区间是左闭右闭的。

在列表的元素前插入元素:

1
2
3
4
5
6
7
8
127.0.0.1:6379> linsert list1 after 1 200
(integer) 5
127.0.0.1:6379> lrange list1 0 -1
1) "3"
2) "2"
3) "1"
4) "200"
5) "1000"

set

set 是 String 类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据。

集合通过哈希表实现,增、删、查的复杂度为 O(1) .

增:

1
2
127.0.0.1:6379> sadd myset1 1 3 5 7 8 3 5 1
(integer) 5

查看个数:

1
2
127.0.0.1:6379> scard myset1
(integer) 5

显示(此例有序系巧合):

1
2
3
4
5
6
127.0.0.1:6379> smembers myset1
1) "1"
2) "3"
3) "5"
4) "7"
5) "8"

其它命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 判断member元素是否是集合key的成员
SISMEMBER key member

# 将member元素从source集合移动到destination集合
SMOVE source destination member

SREM key value # 删除集合中值为value的元素

SRANDMEMBER key num # 在集合中随机选出num个

# 移除并返回集合中一个/num个随机元素
SPOP key [num]

SDIFF key1 key2 # 求差集,key1-key2
SINTER key1 key2 # 求交集
SUNION key1 key2 # 求并集

sorted set(zset)

有序集合和集合一样也是 string 类型元素的集合。

不同的是每个元素都会关联一个double类型的分数。redis 通过分数来为集合中的成员从小到大排序。

有序集合的成员是唯一的,但分数可以重复。

增:

1
2
127.0.0.1:6379> zadd zset1 10 hello 30 world 20 peking
(integer) 3

获取有序集合的成员数:

1
2
127.0.0.1:6379> zcard zset1
(integer) 3

计算在有序集合中指定分数区间的成员数:

1
2
3
4
127.0.0.1:6379> zcount zset1 10 100
(integer) 3
127.0.0.1:6379> zcount zset1 10 15
(integer) 1

查看按照权重排序后的下标对应的元素:

1
2
3
4
5
6
7
8
127.0.0.1:6379> ZRANGE zset1 0 1
1) "hello"
2) "peking"
127.0.0.1:6379> ZRANGE zset1 0 1 withscores
1) "hello"
2) "10"
3) "peking"
4) "20"

通过字典区间返回有序集合的成员(分数要一致):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> zadd zset2 10 ohmyzsh 10 ohmygod 10 hello 10 world
(integer) 4
127.0.0.1:6379> zrangebylex zset2 [h [o
1) "hello"
127.0.0.1:6379> zrangebylex zset2 [h [ok
1) "hello"
2) "ohmygod"
3) "ohmyzsh"
127.0.0.1:6379> zrangebylex zset2 [h [w
1) "hello"
2) "ohmygod"
3) "ohmyzsh"
127.0.0.1:6379> zrangebylex zset2 [h [www
1) "hello"
2) "ohmygod"
3) "ohmyzsh"
4) "world"

容易发现上面的区间是左闭右开的。

其它:

1
2
3
4
5
6
7
# 通过分数返回有序集合指定区间内的成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]

ZSCORE key member # 返回有序集中,成员的分数值

# 返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANGEBYSCORE key max min [WITHSCORES]

hash

hash 是一个 string 类型的 field(字段)和 value(值)的映射表,hash 特别适合用于存储对象。

Key-value 模式不变,但 value 是一个键值对。

Redis 中的 hash 并不是采用哈希实现的,而是类似于以下方式:

1
map<key, map<key1, value>>

HSET key field value将哈希表 key 中的字段 field 的值设为 value :

1
2
127.0.0.1:6379> hset people name Mizuho gender woman age 17
(integer) 3

获取给定字段的值:

1
2
3
4
5
6
127.0.0.1:6379> hget people name
"Mizuho"
127.0.0.1:6379> hget people gender
"woman"
127.0.0.1:6379> hget people age
"17"

获取字段和值:

1
2
3
4
5
6
7
8
127.0.0.1:6379> hkeys people
1) "name"
2) "gender"
3) "age"
127.0.0.1:6379> hvals people
1) "Mizuho"
2) "woman"
3) "17"

配置文件

Units

65-2.png

Network

1
2
3
bind 127.0.0.1      # 绑定的ip
protected-mode yes # 保护模式
port 6379 # 端口

General

1
2
3
daemonize yes     # 以守护进程方式运行
loglevel notice # 日志级别
database 16 # 数据库数量

snapshoting

1
2
3
4
5
6
7
8
save 900 1              
# 900秒(15分钟)后,若至少有1个key发生变化,dump内存快照

save 300 10
# 300秒(5分钟)后,若至少有10个key发生变化,dump内存快照

save 60 10000
# 60秒(1分钟)后,若至少有10000个key发生变化,dump内存快照

(以下信息可能过期):

  • 快照文件名 dbfilename dump.rdb
  • 保存目录名 /var/lib/redis/6379

APPEND ONLY MODE

1
2
3
4
5
6
7
8
9
10
# 是否使用AOF持久化方式。默认不使用
appendonly yes

# 持久化的AOF文件名
appendfilename "appendonly6381.aof"

# 在Redis的配置文件中存在三种AOF同步方式,分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。

持久化

分类、概述

Redis 持久化分为 RDB 持久化和 AOF 持久化:前者将当前数据保存到硬盘(原理是将 Reids 在内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化),后者则是将每次执行的写命令保存到硬盘(原理是将 Reids 的操作日志以追加的方式写入文件,类似于 MySQL 的 binlog);由于 AOF 持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此 AOF 是目前主流的持久化方式,不过 RDB 持久化仍然有其用武之地。

RDB 持久化方式在指定的时间间隔内对数据进行快照存储。

AOF 持久化方式记录每次写操作,服务器重启时会重新执行这些命令以恢复原始数据,AOF 命令以 redis 协议追加。Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。

可以同时开启两种持久化方式。这种情况下,redis 重启时会优先载入 AOF 文件来恢复原始的数据。

RDB 方式

触发 RDB 快照:

  1. 在指定的时间间隔内,执行指定次数的写操作
  2. 执行save(阻塞, 只管保存快照,其他的等待) 或者 bgsave (异步)命令
  3. 执行 flushall 命令,清空数据库所有数据
  4. 执行 shutdown 命令,保证服务器正常关闭且不丢失任何数据

RDB 方式的优点:

  1. 适合大规模的数据恢复,与 AOF 相比,在恢复大的数据集时,RDB 方式更快。
  2. 若业务对数据完整性和一致性要求不高,RDB 是很好的选择。

RDB方式的缺点:

  1. 数据的完整性和一致性不高,因为 RDB 可能在最后一次备份时宕机了。
  2. 备份时占用内存,因为 Redis 在备份时会 fork 一个子进程,将数据写入一个临时文件(此时内存中的数据是原来的两倍),最后再将临时文件替换之前的备份文件。
    65-3.jpeg

AOF 方式

默认不开启 AOF 持久化方式,需要修改配置打开。

默认的 AOF 持久化策略是每秒钟 fsync 一次(把缓存中的写指令记录到磁盘中),因为在这种情况下,redis 仍可以保持高性能,而即使故障,也只会丢失最近 1 秒的数据。

重写(rewrite):
AOF 的运作方式是不断地将命令追加到文件末尾,随着写入命令的增加, AOF 文件也会越来越大。例如,若对一个计数器调用 100 次INCR,那么仅为了保存这个计数器的当前值,AOF 文件就需要使用 100 条记录(entry)。而实际上,只用一条SET命令就足够了。
为此,Redis 支持一种特性,可以在不打断服务客户端的情况下,对 AOF 文件进行重建(rebuild)。

执行BGREWRITEAOF命令, Redis 将生成一个新的 AOF 文件,包含重建当前数据集所需的最少命令。Redis 2.2 需要手动执行BGREWRITEAOF命令;Redis 2.4 则可以自动触发 AOF 重写。

Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于64M时触发:

1
2
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

优点:

  • 保证数据的完整性和一致性
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件易读性好,对文件进行分析(parse)也很轻松。导出(export)AOF 文件也非常简单:例如,若不小心执行了FLUSHALL命令,但只要AOF文件未被重写,那么只要停止服务器,移除 AOF 文件末尾的FLUSHALL命令,并重启 Redis,就可以将数据集恢复到FLUSHALL执行前的状态。

缺点:

  • 大量数据恢复的时候,执行时间长
  • 对于相同的数据集来说,AOF 文件的体积通常大于 RDB 文件的体积

若只有 aof 持久化的方式,且 aof 文件损坏,则 redis 服务器无法启动。

损坏的 aof 文件修复,可尝试:

1
sudo redis-check-aof --fix 文件名

事务

1
2
3
4
5
# 开启事务
multi

# 提交事务
exec

Redis 事务可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行,而不会被其他命令插入,不许加塞。

Redis 事务有以下三个重要的保证:

  1. 批量操作在发送 EXEC 命令前被放入队列缓存。
  2. 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  3. 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务
  2. 命令入队
  3. 执行事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> set k2 hello
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k3 300
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) "300"
3) (integer) 101

redis 的事务不具有原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k2
QUEUED
127.0.0.1:6379> set k4 400
QUEUED
127.0.0.1:6379> get k4
QUEUED
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) "400"
4) (integer) 102
127.0.0.1:6379> get k1
"102"

EXECABORT Transaction discarded because of previous errors :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
127.0.0.1:6379> flushall
OK
127.0.0.1:6379>
127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> set k2 hello
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k4 400
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

取消事务,放弃执行事务块内的所有命令:

1
DISCARD

WATCH key [key ...],监视一个(多个)key ,如果在事务执行之前这个(这些)key 被其他命令改动,那么事务将被打断。

UNWATCH取消WATCH命令对所有 key 的监视。

- - - - - “监视” 演示 - - - - -

开始:

1
2
3
4
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"

terminal 1 :

1
2
3
4
5
6
7
8
127.0.0.1:6379> watch k3
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> set k3 400
QUEUED

terminal 2 :

1
2
3
4
127.0.0.1:6379> set k3 500
OK
127.0.0.1:6379> get k3
"500"

terminal 1 :

1
2
3
4
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get k3
"500"

- - - - - “监视” 演示 END - - - - -

一旦执行EXECWATCH监控会被取消。

乐观锁、悲观锁

悲观锁:
每次拿数据时都会先上锁。其他线程想要访问时,都需要阻塞挂起。传统的关系型数据库里用到了很多这种锁机制,如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。

乐观锁(Optimistic Lock)【冲突检测和数据更新】:
每次拿数据时不上锁。但更新时会使用版本号等机制,判断此期间内该数据是否被更新。乐观锁适用于多读的应用类型,可以提高吞吐量。数据库若提供类似于 write_condition 机制的其实都是乐观锁。

乐观锁策略:提交版本必须大于记录当前版本才能执行更新,一般会使用版本号机制CAS操作实现。

version方式:
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,提交更新时,若刚才读取到的 version 值和当前数据库中的 version 值相等时才真正执行更新,否则重试更新操作,直到更新成功。

CAS(Compare And Swap)操作方式:
CAS 是乐观锁技术,涉及到三个操作数,数据所在的内存值V,预期值A,新值B。当需要更新时,判断当前内存值V与之前取到的值A是否相等,若相等,则用新值更新,若失败则重试。一般情况下是一个自旋操作,即不断的重试。

主从复制、哨兵模式

持久化侧重解决的是 Redis 数据的单机备份问题(从内存到硬盘的备份);而主从复制则侧重解决数据的多机热备。此外,主从复制还可以实现负载均衡和故障恢复。

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


本节课的 PDF 笔记:

常见问题

缓存雪崩

一般而言,热点数据会去做缓存,缓存由定时任务刷新,但定时刷新会产生一个问题:当缓存服务器重启或者大量缓存集中在某一个时间段失效时,此时相当于没有缓存,所有对数据的请求直接走到数据库,带来很大压力。

65-4.png

解决方法:

  • 将缓存失效时间分散开。比如可以在原有的失效时间基础上加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,很难引发集体失效的事件。
  • 不设置缓存的过期时间。有更新操作时就把热点的缓存全部更新,比如首页上的商品,当首页更新时,就把对应的数据替换掉。

缓存击穿

缓存击穿指一个 key 可能会在某些时间点被超高并发地访问,属于“热点”数据,不停地扛着大量并发的访问,当这个热点数据在缓存中过期失效的时候,大量的并发访问就会穿破缓存,转移到数据库上面。

解决方法:

  • 延长热点 key 的过期时间或者设置永不过期,如排行榜、首页等。
  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至 Redis 内,避免其他大量请求同时穿过 Redis 访问底层数据库。

缓存穿透

要查询的数据不存在,缓存无法命中所以需要查询完数据库,但是数据是不存在的,此时数据库肯定会返回空,也就不会记录到缓存中,这样每次对该数据的查询都会穿过缓存去查询一次数据库。

解决方法:

  • 查询时做一些校验和过滤(权限校验,参数校验等等),判断这是一次正常的查询,还是异常的查询或是攻击,如果是不合法的参数或者查询,直接返回。
  • 缓存空对象,如果数据库中不存在这个数据,也在缓存中保存这个 key,只是把 val 值记录为“不存在”、“空”这样的数据,下次再访问这个 key 时,就不会到数据库中做无用的查找了。
  • 可以预先将数据库里面所有的 key 全部存到一个大的 map 里面,然后在过滤器中过滤掉那些不存在的 key. 但是需要考虑数据库的 key 是会更新的,此时需要考虑数据库 —> map 的更新频率问题。类似于位图。

hiredis

安装与使用

1
git clone https://github.com/redis/hiredis.git

进行解压与安装:

1
2
3
4
tar -xzvf hiredis.tar.gz
cd hiredis
make
sudo make install

更新动态库配置文件:

1
sudo ldconfig

按照上面步骤安装之后,hiredis 的头文件会存在 /usr/local/include 下面,hiredis 的库文件存在 /usr/local/lib 下面。

编译方式:
g++ xxx.cc -o xxx -I /usr/local/include/hiredis -lhiredis或者直接g++ xxx.cc -lhiredis 需要链接hiredis的库文件。

后续在代码中引用 hiredis 的头文件,可以直接使用:

1
#include <hiredis/hiredis.h>

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// myRedis.h
#ifndef __MYREDIS_H__
#define __MYREDIS_H__

#include <hiredis/hiredis.h>
#include <string>
#include <iostream>

using std::cout;
using std::endl;
using std::string;

class MyRedis{
public:
MyRedis();
~MyRedis();
bool connect(const string& host, int port);
void set(string key, string value);
string get(string key);

private:
redisContext* _pConnect;
redisReply* _pReply;
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// MyRedis.cc
#include "myRedis.h"

MyRedis::MyRedis():_pConnect(nullptr), _pReply(nullptr){
cout << "MyRedis()" << endl;
}

MyRedis::~MyRedis(){
cout << "~MyRedis()" << endl;

if(_pConnect){
redisFree(_pConnect);
_pConnect = nullptr;
}

if(_pReply){
freeReplyObject(_pReply);
_pReply = nullptr;
}
}

bool MyRedis::connect(const string& host, int port){
_pConnect = redisConnect(host.c_str(), port);
if(_pConnect == nullptr){
return false;
}

if(_pConnect != nullptr && _pConnect->err){
std::cerr << "connect error : " << _pConnect->errstr << endl;
return false;
}

return true; // 连接成功
}

void MyRedis::set(string key, string value){
_pReply = (redisReply*) redisCommand(_pConnect, "SET %s %s", key.c_str(), value.c_str());

if(_pReply){
freeReplyObject(_pReply);
_pReply = nullptr;
}
}

string MyRedis::get(string key){
_pReply = (redisReply*) redisCommand(_pConnect, "GET %s", key.c_str());

if(_pReply){
if(_pReply->type == REDIS_REPLY_STRING){
string str = _pReply->str;

freeReplyObject(_pReply);
_pReply = nullptr;

return str;
}
else {
return nullptr;
}
}

else {
return nullptr;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// testredis.cc
#include "myRedis.h"
#include <memory>

using std::unique_ptr;

void test(){
unique_ptr<MyRedis> pRedis(new MyRedis());

if(!pRedis->connect("127.0.0.1", 6379)){
std::cerr << "connect error ! " << endl;
return;
}

pRedis->set("name", "lili");
cout << "Get the name is " << pRedis->get("name") << endl;
}

int main(int argc, char* argv[]){
test();
return 0;
}

编译运行:

1
2
3
4
5
6
7
wanko@wanko:~/mycode/example_reids$ ls
MyRedis.cc myRedis.h testredis.cc
wanko@wanko:~/mycode/example_reids$ g++ *.cc -lhiredis
wanko@wanko:~/mycode/example_reids$ ./a.out
MyRedis()
Get the name is lili
~MyRedis()

此时回到数据库中查看:

1
2
3
4
5
6
7
wanko@wanko:~$ redis-cli
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> type name
string
127.0.0.1:6379> get name
"lili"

更进一步的拓展:
https://github.com/sewenew/redis-plus-plus