NOSQL

NOSQL

全称:NoSQL = Not Only SQL

泛指非关系型数据库

四大分类:

1)键值(Key-Value)存储数据库

Key/value模型对于IT系统来说的优势在于简单、易部署。
但是如果DBA只对部分值进行查询或更新的时候,Key/value就显得效率低下了。
举例如:Redis.

2)列存储数据库

这部分数据库通常是用来应对分布式存储的海量数据。
键仍然存在,但是它们的特点是指向了多个列。这些列是由列家族来安排的。
如:Cassandra, HBase, Riak.

3)文档型

文档型数据库的灵感是来自于Lotus Notes办公软件的,而且它同第一种键值存储相类似。
该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。
文档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值。
而且文档型数据库比键值数据库的查询效率更高。如:CouchDB, MongoDb.
国内也有文档型数据库SequoiaDB,已经开源。

4)图形(Graph)数据库

图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上。

NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。
许多NoSQL数据库都有REST式的数据接口或者查询API。

我们总结NoSQL数据库在以下的这几种情况下比较适用:

  1. 数据模型比较简单;
  2. 需要灵活性更强的IT系统;
  3. 对数据库性能要求较高;
  4. 不需要高度的数据一致性;
  5. 对于给定key,比较容易映射复杂值的环境。

(十五)Redis数据持久化

(十五)Redis数据持久化

Redis支持数据持久化,可以将内存中的数据持久化到磁盘中,重启的时候再次加载使用。Redis4之前的数据持久化有AOF和RDB两种,从Redis4之后新增了AOF+RDB混合持久化的方式。

上面提到Redis持久化存储有两种持久化方案,RDB(Redis DataBase)和 AOF(Append-Only File)。

  • RDB 内存快照(二进制)

是将内存中的数据进行快照存储到磁盘。

  • AOF 回放日志(文本)

AOF则为可回放的命令日志记录redis内的所有操作。

  • 混合式

AOF 和 RDB各有特点也相互独立。Redis4之后支持RDB-AOF混合持久化的方式,结合了两者的优点,可以通过 aof-use-rdb-preamble 配置项可以打开混合开关。

RDB

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

RDB

RDB(Redis DataBase)是将Redis内存中的数据进行Snaptshot快照存储在磁盘内,是Redis的==默认持久化方案==。使用RDB持久化默认有三种策略,该持久化策略在redis.conf中可配置,会以一段时间内有指定次数据修改的规则触发快照动作,快照文件名为dump.rdb,该文件默认使用LZF压缩算法 。每当Redis服务重启的时候会从该文件中加载数据进内存。

RDB持久化除了可以根据配置中的策略触发,也可以手动触发,使用==save==和==bgsave==命令即可。这两个命令的区别的save会阻塞服务器进程,在进行save的过程中,服务器不能处理任何请求,而bgsave会通过一个子进程在后台处理rdb持久化。事实上save和bgsave调用的都是rdbSave函数,因此Redis不允许save和bgsave同时运行,这也是为了避免出现竞争导致rdb文件数据不准确。

bgsave操作使用CopyOnWrite机制进行写时复制,是由一个子进程将内存中的最新数据遍历写入临时文件,此时父进程仍旧处理客户端的操作,当子进程操作完毕后再将该临时文件重命名为dump.rdb替换掉原来的dump.rdb文件,因此无论bgsave是否成功,dump.rdb都不会受到影响。

另外在主从全量同步、debug reload以及shutdown的情况下也会触发RDB数据持久化。

配置RDB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim $REDIS_HOME/bin/redis.conf


#RDB持久化策略 默认三种方式,
[900秒内有1次修改],
[300秒内有10次修改],
[60秒内有10000次修改]即触发RDB持久化,
我们可以手动修改该参数或新增策略

save 900 1
save 300 10
save 60 10000

#RDB文件名
dbfilename "dump.rdb"

#RDB文件存储路径
dir "/opt/app/redis6/data"

AOF

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF

AOF(Append-Only File)记录Redis中每次的写命令,类似mysql中的binlog,服务重启时会重新执行AOF中的命令将数据恢复到内存中,RDB(按策略持久化)持久化方式记录的粒度不如AOF(记录每条写命令),因此很多生产环境都是开启AOF持久化。

AOF中记录了操作和数据,在日志文件中追加完成后才会将内存中的数据进行变更。

AOF持久化流程:

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

配置AOF

开启了AOF之后,RDB就默认不使用了。使用下面的配置开启AOF以及策略。(如果使用AOF,推荐选择always方式持久化,否则在高并发场景下,每秒钟会有几万甚至百万条请求,如果使用everysec的方式的话,万一服务器挂了那几万条数据就丢失了)。

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
vim $REDIS_HOME/bin/redis.conf

#开启AOF持久化
appendonly yes

#AOF文件名
appendfilename "appendonly.aof"

#AOF文件存储路径 与RDB是同一个参数
dir "/opt/app/redis6/data"

#AOF策略,一般都是选择第一种
[always:每个命令都记录],
[everysec:每秒记录一次],
[no:看机器的心情高兴了就记录]
appendfsync always
#appendfsync everysec
# appendfsync no


#aof文件大小比起上次重写时的大小,增长100%(配置可以大于100%)时,触发重写。
[假如上次重写后大小为10MB,当AOF文件达到20MB时也会再次触发重写,以此类推]
auto-aof-rewrite-percentage 100

#aof文件大小超过64MB时,触发重写
auto-aof-rewrite-min-size 64mb

优缺点

RDB

优点

1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

缺点

1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF

优点

1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

缺点

1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。不过生产环境其实更多都是二者结合使用的。

常用配置

RDB持久化配置

Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:

save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。

AOF持久化配置

在Redis的配置文件中存在三种同步方式,它们分别是:

appendfsync always #每次有数据修改发生时都会写入AOF文件。

appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。

appendfsync no #从不同步。高效但是数据不会被持久化。

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

参考

缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

缓存正常从Redis中获取,示意图如下:

image

缓存失效瞬间示意图如下:

image

解决方案:

(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object getProductListNew(){
int cacheTime = 30;
String cacheKey = "product_list";
String localKey = cacheKey;

String cacheValue = CacheHelper.get(cacheKey);
if(cacheValue != null){
return cacheValue;
} else {
sychronized(localKey){
if(cacheValue != null){
return cacheValue;
} else {
cacheValue = getProductListFromDB();
CacheHelper.add(cacheKey, cacheValue, cacheTime);
}

}
return cacheValue;
}

}

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2)给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

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
public Object getProductListNew(){
int cacheTime = 30;
String cacheKey = "product_list";

//缓存标记
String cacheSign = cacheKey + "_sign";

String sign = CacheHelper.get(cacheSign);

String cacheValue = CacheHelper.get(cacheKey);

if(sign != null){
return cacheValue;
} else {
//过期
CacheHelper.add(cacheSign, 1, cacheTime);
ThreadPool.QueueUserWorkItem((arg -> {
cacheValue = getProductListFromDB();
CacheHelper.add(cacheKey, cacheValue, cacheTime * 2);
}));

return cacheValue;
}

}

解释说明:

1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。

缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

缓存穿透解决方案:

(1)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

(2)如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴! 把空结果也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。

缓存预热

缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存预热解决方案:

  • (1)直接写个缓存刷新页面,上线时手工操作下;

  • (2)数据量不大,可以在项目启动的时候自动进行加载;

  • (3)定时刷新缓存;

缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

(1)定时去清理过期的缓存;

(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

如何使用 Redis 实现分布式锁

如何使用 Redis 实现分布式锁

锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现,在没有保护的操作的前提下,可能会出现不可预知的问题。

而现代系统大多为分布式系统,这就引入了分布式锁,要求具有在分布各处的服务上保护资源的能力。

而实现分布式锁,目前大多有以下三种方式:

  • 使用数据库实现。
  • 使用 Redis 等缓存系统实现。
  • 使用 Zookeeper 等分布式协调系统实现。

其中 Redis 简便灵活,高可用分布式,且支持持久化。

设计锁

锁本身是很简单的,就是redis数据库中一个简单的key。建立和释放锁,并保证绝对的安全,是这个锁的设计比较棘手的地方。有两个潜在的陷阱:

  • 应用程序通过网络和redis交互,这意味着从应用程序发出命令到redis结果返回之间会有延迟。这段时间内,redis可能正在运行其他的命令,而redis内数据的状态可能不是你的程序所期待的。如果保证程序中获取锁的线程和其他线程不发生冲突?

  • 如果程序在获取锁后突然crash,而无法释放它?这个锁会一直存在而导致程序进入“饿死”(原文成为“死锁”,感觉不太准确)。

建立锁

可能想到的最简单的方法是“用GET方法检查锁,如果锁不存在,就用SET方式设置一个值”。

这个方法虽然简单,但是不能保证独占锁。

回顾前面所说的第1个陷阱:因为在GET和SET操作之间有延迟,我们没法知道从“发送命令”到“redis服务器返回结果”之间的这段时间内是否有其他线程也去建立锁。当然,这些都在几毫秒之内,发生的可能性相当低。但是如果在一个繁忙的环境中运行着大量的并发线程和命令,重叠的可能性并不是微不足道的。

为了解决这个问题,应该用SETNX命令。SETNX消除了GET命令需要等待返回值的问题,SETNX只有在key不存在时才返回成功。这意味着只有一个线程可以成功运行SETNX命令,而其他线程会失败,然后不断重试,直到它们能建立锁。

释放锁

一旦线程成功执行了SETNX命令,它就建立了锁并且可以基于资源进行工作。工作完成后,线程需要通过删除redis的key来释放这个锁,从而允许其他线程能尽快的获取锁。

尽管如此,也有需要小心的地方!回顾前面说的第2个陷阱:如果线程crash了,它永远都不会删除redis的key,所以这个锁会一直存在,从而导致“饿死”现象。那么如何避免这个问题呢?

锁的存活时间

我们可以给锁加一个存活时间(TTL),这样一旦TTL超时,这个锁的key会被redis自动删除。任何由于线程错误而遗留下来的锁在一个合适的时间之后都会被释放,从而避免了“饿死”。这纯粹是一个安全特性,更有效的方式仍然是确保尽量在线程里面释放锁。

可以通过PEXPIRE命令为Redis的key设置TTL,而且线程里可以通过MULTI/EXEC事务的方式在SETNX命令后立即执行,例如:

1
2
3
4
MULTI
SETNX lock-key
PEXPIRE 10000 lock-key
EXEC

尽管如此,这会产生另外一个问题。PEXPIRE命令没有判断SETNX命令的返回结果,无论如何都会设置key的TTL。如果这个地方无法获取到锁或有异常,那么多个线程每次想获取锁时,都会频繁更新key的TTL,这样会一直延长key的TTL,导致key永远都不会过期。为了解决这个问题,我们需要Redis在一个命令里面处理这个逻辑。我们可以通过Redis脚本的方式来实现。

注意-如果不采用脚本的方式来实现,可以使用Redis 2.6.12之后版本SET命令的PX和NX参数来实现。为了考虑兼容2.6.0之前的版本,我们还是采用脚本的方式来实现。

Redis脚本
由于Redis支持脚本,我们可以写一个Lua脚本在Redis服务端运行多个Redis命令。应用程序通过一条EVALSHA命令就可以调用被Redis服务端缓存的脚本。这里强大的地方在于你的程序只需要运行一条命令(脚本)就可以以事务的方式运行多个redis命令,还能避免并发冲突,因为一个redis脚本同一时刻只能运行一次。

这是Redis里面一个设置带TTL的锁的Lua脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--
-- Set a lock
--
-- KEYS[1] - key
-- KEYS[2] - ttl in ms
-- KEYS[3] - lock content
local key = KEYS[1]
local ttl = KEYS[2]
local content = KEYS[3]

local lockSet = redis.call('setnx', key, content)

if lockSet == 1 then
redis.call('pexpire', key, ttl)
end

return lockSet

从这个脚本可以很清楚的看到,我们通过在锁上只运行PEXPIRE命令就解决了前面提到的“无休止的TTL”问题。

释放锁

原文中还缺少一个释放锁的脚本,如果一直依赖TTL来释放锁,效率会很低。Redis的SET操作文档就提供了一个释放锁的脚本:

1
2
3
4
5
6
if redis.call("get", KEYS[1]) == ARGV[1]
then
return redis.call("del", KEYS[1])
else
return 0
end

应用程序中只要加锁的时候指定一个随机数或特定的value作为key的值,解锁的时候用这个value去解锁就可以了。当然,每次加锁时的value必须要保证是唯一的。

(十四)Redis实现分布式锁方案比较

(十四)Redis实现分布式锁方案比较

SETNX 语义

1
SETNX key value

命令执行时,如果 ==key== 不存在,则设置 key 值为 value(同set);如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识。

1
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

==NX== - 仅在 key 不存在时执行赋值操作。命令描述文档 而如下文所述,通过SET的NX选项使用,可同时使用其它选项,如EX/PX设置超时时间,是更好的方式。

SETNX 实现分布式锁

方案1:SETNX + delete

伪代码如下:

1
2
3
setnx lock_a random_value
// do sth
delete lock_a

此实现方式的问题在于:一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。

方案2:SETNX + SETEX

伪代码如下:

1
2
3
4
setnx lock_a random_value
setex lock_a 10 random_value // 10s超时
// do sth
delete lock_a

按需设置超时时间。此方案解决了方案1死锁的问题,但同时引入了新的死锁问题: 如果setnx之后,setex 之前服务挂掉,会陷入死锁。 根本原因为 setnx/setex 分为了两个步骤,非原子操作。

方案3:SET NX PX

伪代码如下:

1
2
3
SET lock_a random_value NX PX 10000 // 10s超时
// do sth
delete lock_a

此方案通过 set 的 NX/PX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案1、2的问题。(PX与EX选项的语义相同,差异仅在单位。)
此方案目前大多数 sdk、redis 部署方案都支持,因此是推荐使用的方式。
但此方案也有如下问题:
如果锁被错误的释放(如超时),或被错误的抢占,或因redis问题等导致锁丢失,无法很快的感知到。

SET key randomvalue NX PX

方案4在3的基础上,增加对 value 的检查,只解除自己加的锁。
类似于 CAS,不过是 compare-and-delete。
此方案 redis 原生命令不支持,为保证原子性,需要通过lua脚本实现:。
伪代码如下:

1
2
3
SET lock_a random_value NX PX 10000
// do sth
eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_a random_value

此方案更严谨:即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。

注意事项

超时时间

从上述描述可看出,超时时间是一个比较重要的变量:
超时时间不能太短,否则在任务执行完成前就自动释放了锁,导致资源暴露在锁保护之外。
超时时间不能太长,否则会导致意外死锁后长时间的等待。除非人为接入处理。
因此建议是根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。
如果实在无法确定而又要求比较严格,可以采用定期 setex/expire 更新超时时间实现。

重试

如果拿不到锁,建议根据任务性质、业务形式进行轮询等待。
等待次数需要参考任务执行时间。

与redis事务的比较

setnx 使用更为灵活方案。multi/exec 的事务实现形式更为复杂。
且部分redis集群方案(如codis),不支持multi/exec 事务。

(十)Redis持久化命令

(十)Redis持久化命令

数据持久化

异步AOF

执行一个异步的AOF(append only file)文件重写

1
bgrewriteaof

同步RDB

同步RDB持久化数据到磁盘,同步地将redis中的数据持久化到磁盘

1
save

异步RDB

异步RDB持久化数据到磁盘,异步将redis中的数据持久化到磁盘

1
bgsave

查看上次RDB持久化时间

使用lastsave命令查看上次持久化到磁盘的时间:

1
lastsave

(五)Redis数据类型之Hash

(五)Redis数据类型之Hash

hexists

hexists key field

查看hash类型的key中指定的field是否存在,返回true / false, 1/0

hset

hset key field value [field value …]

可设置单个field, 也可以设置多个值

hsetnx

hsetnx key field value

只有不存在的field才会被创建,若field已存在则不做任何动作

hdel

hdel key field[field2 …]

删除map中指定field的数据, 可以删除多个

hget

hget key field

获取指定field的值

hmget

hmget key field1 [field2 …]

获取指定多个field的值

hgetall

hgetall key

获取指定hashmap的全部field和value

hkeys

hkeys key

获取指定hash类型对象的全部field

hvals

hvals key

获取指定hash类型对象的全部value

hincrby

hincrby key field increment

对HashMap指定的field对应的value做增加操作,increment是整数, increment为负数,则为减少操作,value必须是integer类型。

hincrbyfloat

hincrbyfloat key field increment

对HashMap指定的field对应的value做增加操作,increment是整s数或者浮点数, increment为负数,则为减少操作,value必须是数字类型。

hlen

hlen key

计算field数量

hstrlen

hstrlen key field

获取map中指定field对应value的字符长度

expire

expire key seconds

我们可以看到hash类型没有hsetex hpsetex一类的方法,想对hash对象做过期策略可以使用全局函数expire,单位为秒。

(二)Redis 数据类型之字符串List

(二)Redis 数据类型之字符串List

List的实现原理是双向循环链表

List

image

查找方向可以是从左往右也可以是从右往左,但是要实现从右往左还需要终端节点的地址,所以通常会设计成双向的循环链表;

若链表为空:

image

lpush

1
2
3
4
5
6
7
8
9
10
11
12
lpush key element [element ...]

lpush mylist 'test'
lpush mylist 'test2'
lpush mylist 'test3'
lpush mylist 'test4'
lrange mylist 0 -1

1) "test4"
2) "test3"
3) "test2"
4) "test"

从左边插入元素, 从左边依次追加进栈,先进后出,后进先出

rpush

1
2
3
4
5
6
7
8
9
10
11
12
rpush key element [element ...]

rpush mylist2 'test'
rpush mylist2 'test2'
rpush mylist2 'test3'
rpush mylist2 'test4'
lrange mylist2 0 -1

1) "test"
2) "test2"
3) "test3"
4) "test4"

从右边插入元素, 从右边依次追加进队列,先进先出,后进后出

lrange

根据起止下标查询列表元素

LRANGE key start stop

start: 从指定下标开始检索
stop: 检索几个元素 -1 表示全部元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lrange key 0 -1 #表示查看全部元素
lrange key -1 -1 #表示查看最右边的元素

lrange mylist2 0 -1

1) "-1"
2) "0"
3) "0"
4) "test"
5) "test2"
6) "test3"
7) "test4"
8) "test5"
9) "test6"

lrange mylist2 -1 -1

1) "test6"

lpushx

1
lpushx key element [element ...]

与string类型中的nx类似,只有当list存在时才会从左边依次追加元素。

rpushx

1
rpushx key element [element ...]

与string类型中的nx类似,只有当list存在时才会从右边依次追加元素。

linsert

从list中指定的元素的前/后 插入一个新元素:

linsert key BEFORE|AFTER pivot element

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lrange mylist3 0 -1 查看列表
1) "-1"
2) "0"
3) "1"

linsert mylist3 before -1 -2-1的前面插入-2
lrange mylist3 0 -1
1) "-2"
2) "-1"
3) "0"
4) "1"

linsert mylist3 after 1 21的后面插入2
lrange mylist3 0 -1
1) "-2"
2) "-1"
3) "0"
4) "1"
5) "2"

lrem

lrem key count element

从指定列表左侧开始,删除count个指定元素element

1
2
3
4
5
6
7
8
9
10
11
12
13
lrem mylist3 1 2 从左侧开始删除12
lrange mylist3 0 -1

1) "-2"
2) "-1"
3) "0"
4) "1"
lrem mylist3 1 -2 从左侧开始删除1-2
lrange mylist3 0 -1

1) "-1"
2) "0"
3) "1"

lset

lset key index element

根据下标修改元素内容,下标从左边算起,以0开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lrange mylist 0 -1
1) "-1"
2) "test4"
3) "test3"
4) "test2"
5) "test"

lset mylist 1 test 设置第一个元素为test
lrange mylist 0 -1
1) "-1"
2) "test"
3) "test3"
4) "test2"
5) "test"

ltrim

ltrim key start stop

将原列表截取为从下标start到下标stop闭区间的列表

1
2
3
4
5
6
7
8
9
10
11
lrange mylist 0 -1
1) "-1"
2) "test"
3) "test3"
4) "test2"
5) "test"
ltrim mylist 0 2 ## 截取从0-2的元素为一个新的List
lrange mylist 0 -1
1) "-1"
2) "test"
3) "test3"

llen

llen key

查看指定List中元素的个数

1
llen mylist

lindex

llindex key index
根据指定数组index检索元素

1
2
3
4
5
6
lrange mylist 0 -1
1) "-1"
2) "test"
3) "test3"
lindex mylist 0 ##查找第0个元素
"-1"

lpop

lpop key

左侧消费数据,消费完删除, 这里可以把List当成一个消息队列去看。

rpop

rpop key

从右侧消费数据,消费完删除

(三) Redis数据类型之List下

rpoplpush

消费列表A的最右边的元素返回,然后追加到列表B的最左边:

rpoplpush source destination

rpoplpush List_A List_B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Docker:0>lrange source 0 -1
1) "1"
2) "2"
3) "3"
Docker:0>lrange destination 0 -1
1) "4"
2) "5"
3) "6"
Docker:0>rpoplpush source destination
"3"

Docker:0>lrange source 0 -1
1) "1"
2) "2"
Docker:0>lrange destination 0 -1
1) "3"
2) "4"
3) "5"
4) "6"

blpop

blpop key timeout

列表左侧查询元素,返回列表的key和左侧第一个元素。若所有查询的列表中都没有元素,则会阻塞等待至设置的timeout秒之后返回空,若在这期间,这些列表新增了元素,则会立刻消费并返回该元素。

brpop

brpop key timeout

类似,从右侧消费。

brpoplpush

brpoplpush source destination timeout
结合brpop和lpush,阻塞消费并将消费到的元素添加至target列表的最左侧:

(四)Redis 数据类型之List实现数据结构

(四)Redis 数据类型之List实现数据结构

栈:先进后出

lpush + lpop

左侧入栈,左侧出站

队列:先进先出

lpush + rpop
左侧入栈,右侧出栈,队列思想

有限集合

lpush + ltrim

消息队列

lpush + brpop

左侧不断入栈,通过brpop不断消费List中数据