Ubuntu 18.04 手动安装最新版 Redis

Redis ,全称 REmote DIctionary Server ,是一个由 Salvatore Sanfilippo 编写的开源高性能 key-value 存储系统。Redis 是基于内存的 Key-Value 数据库,比 Memcache 更先进,支持多种数据结构,高效,快速。用 Redis 可以很轻松解决高并发的数据访问问题,做为时时监控信号处理也非常不错。本文详细介绍如何在 Ubuntu 18.04 上手动安装最新版 Redis 。以下操作是在 root 账号下进行的,非 root 账号需提升到 root 权限。

安装 Redis

Ubuntu 18.04 默认源中的 Redis 版本不是最新版,要想通过 apt-get install 的方式安装最新版,首先添加 Redis 源。

添加 Redis 源

首先安装依赖:

apt-get install software-properties-common -y

使用如下命令添加 Redis 源:

add-apt-repository ppa:chris-lea/redis-server -y

安装 Redis

使用如下命令安装 Redis:

apt-get update && apt-get install redis-server -y

通过以上命令便可安装最新版 Redis ,终端中输入以下命令查询 Redis 版本:

redis-server -v

命令执行后返回 Redis 版本如下,已经是最新版本:

Redis server v=5.0.3 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=45d60903d31a0894

Redis 安装完成后服务已经自动启动,通过以下命令查看 Redis 服务状态:

systemctl status redis

命令执行后会返回类似下面的信息,可以看到 Redis 服务已经启动,而且重启系统服务也会自动运行:

redis-server.service - Advanced key-value store
   Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2019-01-19 10:48:52 UTC; 12s ago
     Docs: http://redis.io/documentation,
           man:redis-server(1)
  Process: 2421 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS)
  Process: 2424 ExecStart=/usr/bin/redis-server /etc/redis/redis.conf (code=exited, status=0/SUCCESS)
 Main PID: 2445 (redis-server)
    Tasks: 4 (limit: 4704)
   CGroup: /system.slice/redis-server.service
           └─2445 /usr/bin/redis-server 127.0.0.1:6379

配置 Redis 密码

修改配置文件,设置 Redis 密码

配置 Redis 密码将启用 auth 命令,该命令需要客户端进行身份验证才能访问数据库,大大提高安全性。配置 Redis 密码是通过修改 Redis 配置文件完成的。Redis 配置文件为 /etc/redis/redis.conf 。在 redis.conf 文件中有一条对 requirepass 的注释警告:

# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.

因此,指定一个非常强大和非常长的值作为密码很重要,可以使用 openssl 命令生成一个随机密码:

printf "$(openssl rand 60 | openssl base64 -A) n" 

命令执行后终端返回一串随机字符串,例如:

EDyHgJqw1maAYr+bbjmIKF+1lm9EnGegFrzdy2zNfAdgvCCi1Wz+Xezs1YzVpHPpDZqyl2uNSwRIKSFn

将该随机字符串做为 Redis 密码(注意,以下命令执行时请将随机字符串修改为自己获取的真实随机字符串):

sed -i 's/# requirepass foobared/requirepass EDyHgJqw1maAYr+bbjmIKF+1lm9EnGegFrzdy2zNfAdgvCCi1Wz+Xezs1YzVpHPpDZqyl2uNSwRIKSFn/' /etc/redis/redis.conf

重启 Redis 服务,使密码生效:

systemctl restart redis.service

测试 Redis 密码

终端中输入执行以下命令进入 Redis 客户端:

redis-cli

终端中返回结果如下:

root@timelate:~# redis-cli
127.0.0.1:6379> 

输入 ping 并回车,终端中返回结果如下:

127.0.0.1:6379> ping
(error) NOAUTH Authentication required.

提示需要认证,说明密码已经生效。继续在终端中输入以下命令,进行密码认证:

auth EDyHgJqw1maAYr+bbjmIKF+1lm9EnGegFrzdy2zNfAdgvCCi1Wz+Xezs1YzVpHPpDZqyl2uNSwRIKSFn

命令执行后终端返回结果如下,提示认证成功:

127.0.0.1:6379> auth EDyHgJqw1maAYr+bbjmIKF+1lm9EnGegFrzdy2zNfAdgvCCi1Wz+Xezs1YzVpHPpDZqyl2uNSwRIKSFn
OK

终端中输入 exit 并回车,退出 Redis 客户端。

通过以上步骤便可在 Ubuntu 18.04 上手动安装最新版 Redis ,本文结束。

redis(一)–在centos7.5下编译安装redis5.0

1、安装依赖

首先要安装redis需要依赖的软件包

yum install gcc*

2、下载并解压

首先下载

wget http://download.redis.io/releases/redis-5.0.0.tar.gz

然后解压开包

tar -zxvf redis-5.0.0.tar.gz

cd redis-5.0.0

执行命令make MALLOC=libc

如果不添加MALLOC=libc就会提示下边的错误。

未分类

3、启停redis

启动redis

./src/redis-server $PWD/redis.conf

这样启动不会是后台启动

nohup ./src/redis-server $PWD/redis.conf &

这样就是后台启动

以上启动是一种方法,还有一种方法在配置文件中进行修改。

# 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

默认的是daemonize no,将no 改成yes。

关闭redis

./src/redis-cli -h ip_address -p ip_port shutdown

在关闭的时候shutdown必须要在此命令的最后边,否则这个命令会识别成默认的127.0.0.1去关闭。

4、redis 启动脚本

一般在解压包的utils中会有一个简单的启动脚本

cp /usr/local/src-new/redis-5.0.0/utils/redis_init_script redis

将文件中的启动命令等相关参数进行更改。

Redis数据库解析,读懂直接去见面试官

上一篇,我们简单了解过NoSQL数据库下文档型数据库MongoDB,今天我们来介绍下NoSQL数据库下一款缓存数据库Redis。谈到Redis,应该没有一个技术开发者会否认,在当今的技术架构中,Redis已然成为使用最广泛的缓存,它支持复杂的数据结构,支持持久化,支持主从集群,支持高可用……

在介绍Redis之前,我们不妨先简要的了解下缓存。随着互联网的普及,信息内容愈加复杂,用户数量和访问数量不断增加,所以我们的应用服务器和数据库服务器所做的工作也就越来越多,然而服务器的工作能力是有限的,数据库每秒处理请求的次数也是有限的,进而有效利用有限资源来实现最大限度的吞吐量成为急需解决的问题。那么我们就可以考虑引入缓存,实现每一个请求都可以通过缓存完成高效处理工作。

所谓缓存,就是如果数据没有发生本质的话,我们就无需去数据库中查询,而直接就可以通过内存读取数据,这样就可以大大降低数据库的读写次数,工作效率能提高很多。Redis作为一款缓存数据库,大量使用内存间的数据快速读写,支持高并发大吞吐,并能够对缓存提供持久化支持。下面我们从多维度来了解下Redis。

什么是Redis

Redis的全称是Remote Dictionary Server,由意大利人 Salvatore Sanfilippo 使用C语言开发。Redis是完全免费开源的,遵守BSD协议。本质上讲,Redis是一个key_value型单线程数据库,可以用作数据库、缓存和消息的中间件,属当前最受欢迎的NoSql数据库之一,也被人们称为数据结构服务器。

未分类

Redis的数据类型

Redis之所以备受欢迎,其中最大的魅力在于它支持保存五种基本数据结构类型。string(字符串),hash(哈希),list(列表),set(集合)和zset(sorted set有序集合)。

1.string

它是redis的最基本的数据类型,一个键对应一个值,需要注意是一个键值最大存储512MB。一般用于一些复杂的计数功能的缓存;

2.hash

redis hash是一个键值对的集合,是一个string类型的field和value的映射表,这里的value存放的是结构化的对象,操作其中某个字段十分方便,适合用于存储对象;

3.list

是Redis类相对简单的字符串列表,通过list可以做简单的消息列队功能。list对应的是一个双向列表,按照插入顺序排序;

4.set

是string类型的无序集合,集合中的数据不能重复出现,所以可以用来做全局的去重功能;

5.zset

是string类型的有序集合,同set一样不可重复。有序集合中排序因子为每个元素附带一个double型的分数,根据分数对元素进行升序排序。如果多个元素有相同的分数,就会通过字典序完成升序排序,因此sorted set十分适合做排行榜应用。sorted set也可用来做延时任务。

未分类

速度解析

Redis作为一款单线程模型数据库,还有另外一处魅力所在,那就是快。很多人会对此产生疑问,为什么Redis是单线程还很快,关于这个问题,我们从以下几点展开分析:

  1. Redis使用标准C语言写,所有数据都在内存中完成,读写速度分别达到10万/20万,是已知性能最快的Key-Value DB ;

  2. 单线程操作,避免了频繁的上下文切换,也不用考虑在多进程或者多线程中导致的切换而消耗 cpu;

  3. 核心采用基于非阻塞的 IO 多路复用机制,单个线程通过跟踪每个I/O的状态,来管理多个I/O流。

过期删除策略

在Redis内,我们可以使用EXPIRE或EXPIREAT设置key的过期时间,Redis内存达到maxmemory限制后,Redis内存就会施行过期数据淘汰策略。过期删除策略通常有三种:定时删除、惰性删除、定期删除,目前Redis使用的过期键删除策略为惰性删除和定期删除,两种策略配合使用。

惰性删除,指当某个key值过期之后,该key值不会马上被删除,只有当读或者写一个已经过期的key时,才会触发惰性删除策略,此时该key完成删除。

定期删除,指每隔一段时间就会扫描一定数量数据库的expires字典内一定数量的key,并删除里面过期的key。

由于惰性删除无法保证过期数据被及时删除掉,所以Redis采用惰性删除,定期删除相结合的方法,可以帮助实现定期主动淘汰已过期的key,从而使cpu和内存资源达到最优的平衡效果。

InfoQ 中文站曾就Redis应用采访过新浪微博开放平台资深工程师唐福林,唐福林在采访中表示,个人是十分认可Redis丰富数据结构和高速读写功能这两大优势的,并且在长期的技术研发中,新浪微博会不断推进RedisCounter的开发和一些存储列表类数据的 eRedis 的定制开发。Redis深受众多技术开发者的喜爱,在我们熟悉的TitanFramework中,只需实现StorageDataProcessor,满足Redis获取接口中已经实现的manager方法进行数据操作,就能实现Redis的完美接入。

一文让你明白Redis持久化

网上虽然已经有很多类似的介绍了,但我还是自己总结归纳了一下,自认为内容和细节都是比较齐全的。

文章篇幅有 4k 多字,货有点干,断断续续写了好几天,希望对大家有帮助。不出意外地话,今后会陆续更新 Redis 相关的文章,和大家一起学习。

好了,下面开始回归正文:

Redis 一共有 2 种持久化方式,分别是 RDB 和 AOF,下面我来详细介绍两种方式在各个过程所做的事情,特点等等。

1. RDB 持久化

RDB 持久化是 Redis 默认的持久化方式。

它所生成的 RDB 文件是一个压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态

PS:数据库状态是指 Redis 服务器的非空数据库以及他们键值对的统称

1.1 RDB 文件的创建

有两个命令可以生成 RDB 文件,一个是 SAVE、另一个是 BGSAVE。

两者的区别在于:前者会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止。

而在服务器进程阻塞期间,服务器是不能处理任何命令请求的。

后者则不会阻塞服务器进程,因为是通过 fork 一个子进程,并让其去创建 RDB 文件,而服务器进程(父进程)继续则继续处理命令请求。

当写完数据库状态后,新 RDB 文件就会原子地替换旧的 RDB 文件。

此处小提问:如果在执行 BGSAVE 期间,客户端发送 SAVE、BGSAVE 或 BGREWRITEAOF 命令给服务端,服务端会如何处理呢?
答案:在执行 BGSAVE 期间,上述三个命令都不会被执行。

详细原因:前两个会被直接拒绝,原因是为了避免父子进程同时执行两个 rdbSave 调用,防止产生竞争条件。 
而 BGREWRITEAOF 命令则是会被延迟到 BGSAVE 命令执行之后再执行。 
但如果是 BGREWRITEAOF 命令正在执行,此时客户端发送 BGSAVE 命令则会被拒绝。 
因为 BGREWRITEAOF 和 BGSAVE 都是由子进程执行的,所以在操作方面没有冲突的地方,不能同时执行的原因是性能上的考虑——并发出两个子进程,并且这两个子进程都会同时执行大量 io(磁盘写入)操作

1.2 RDB 文件的载入

RDB 文件的载入是在服务器启动时自动执行的,所以没有用于载入的命令,期间阻塞主进程。

只要没有开启 AOF 持久化功能,在启动时检测到有 RDB 文件,就会自动载入。

当服务器有开启 AOF 持久化功能时,服务器将会优先使用 AOF 文件来还原数据库状态。原因是 AOF 文件的更新频率通常比 RDB 文件的更新频率高。

1.3 自动间隔性保存

对于 RDB 持久化而言,我们一般都会使用 BGSAVE 来持久化,毕竟它不会阻塞服务器进程。

在 Redis 的配置文件,有提供设置服务器每隔多久时间来执行 BGSAVE 命令。

Redis 默认是如下配置:

save 900 1 // 900 秒内,对数据库至少修改 1 次。下面同理 
save 300 10 
save 60 10000

只要满足其中一种情况,服务器就会执行 BGSAVE 命令。

2. AOF 持久化

我们从上面的介绍知道,RDB 持久化通过保存数据库状态来持久化。而 AOF 与之不同,它是通过保存对数据库的写命令来记录数据库状态。

比如执行了 set key 123,Redis 就会将这条写命令保存到 AOF 文件中。

在服务器下次启动时,就可以通过载入和执行 AOF 文件中保存的命令,来还原服务器关闭前的数据库状态了。

总体流程和 RDB 持久化一样 —— 都是创建一个 xxx 文件、在服务器下次启动时就载入这个文件来还原数据

那么,AOF 持久化具体是怎么实现的呢?

2.1 AOF 持久化实现

AOF 持久化功能的实现可以分为 3 个步骤:命令追加、文件写入、文件同步

其中命令追加很好理解,就是将写命令追加到 AOF 缓冲区的末尾。

那文件写入和文件同步怎么理解呢?刚开始我也一脸懵逼,终于在网上找到了答案,参考见文末,有兴趣的读者可以去看看。

先不卖关子了,简单一句话解释就是:前者是缓冲区内容写到 AOF 文件,后者是将 AOF 文件保存到磁盘

ok,明白什么意思之后,我们稍微详细看下这两个东西是什么鬼。

在《Redis设计与实现》中提到,Redis 服务器进程就是一个事件循环,这个循环中的文件事件(socket 的可读可写事件)负责接收客户端的命令请求,以及向客户端发送命令结果。

因为服务器在处理文件事件时,可能会发生写操作,使得一些内容会被追加到 AOF 缓冲区末尾。所以,在服务器每次结束一个事件循环之前 ,都会调用 flushAppendOnlyFile 方法。

这个方法执行以下两个工作:

  • WRITE:根据条件,将缓冲区内容写入到 AOF 文件。
  • SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。

两个步骤都需要根据一定的条件来执行,而这些条件由 Redis 配置文件中的 appendfsync 选项来决定的,一共有三个选择:

  1. appendfsync always:每执行一个命令保存一次
  2. appendfsync everysec(默认,推荐):每一秒钟保存一次
  3. appendfsync no:不保存

下面说下三个的区别:

  • appendfsync always:每次执行完一个命令之后, WRITE 和 SAVE 都会被执行
  • appendfsync everysec:SAVE 原则上每隔一秒钟就会执行一次。
  • appendfsync no:每次执行完一个命令之后, WRITE 会执行,SAVE 都会被忽略,只会在以下任意一种情况中被执行:
    • Redis 被关闭
    • AOF 功能被关闭
    • 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行。完成依赖 OS 的写入,一般为 30 秒左右一次)

而对于操作特性来分析的话,则是如下情况:

未分类

既然 AOF 持久化是通过保存写命令到文件的,那随着时间的推移,这个 AOF 文件记录的内容就越来越多,文件体积也就越来越大,对其进行数据还原的时间也就越来越久。

针对这个问题,Redis 提供了 AOF 文件重写功能。

2.2 AOF 重写

通过该功能来创建一个新的 AOF 文件来代替旧文件。并且两个文件所保存的数据库状态一样,但新文件不会包含任何冗余命令,所以新文件要比旧文件小得多。

而为什么新文件不会包含任何冗余命令呢?

那是因为这个重写功能是通过读取服务器当前的数据库状态来实现的。虽然叫做「重写」,但实际上并没有对旧文件进行任何读取修改。

比如旧文件保存了对某个 key 有 4 个 set 命令,经过重写之后,新文件只会记录最后一次对该 key 的 set 命令。因此说新文件不会包含任何冗余命令

因为重写涉及到大量 IO 操作,所以 Redis 是用子进程来实现这个功能的,否则将会阻塞主进程。该子进程拥有父进程的数据副本,可以避免在使用锁的情况下,保证数据的安全性。

那么这里又会存在一个问题,子进程在重写过程中,服务器还在继续处理命令请求,新命令可能会对数据库进行修改,这会导致当前数据库状态和重写后的 AOF 文件,所保存的数据库状态不一致

为了解决这个问题,Redis 设置了一个 AOF 重写缓冲区。在子进程执行 AOF 重写期间,主进程需要执行以下三个步骤:

  1. 执行客户端的请求命令
  2. 将执行后的写命令追加到 AOF 缓冲区
  3. 将执行后的写命令追加到 AOF 重写缓冲区

当子进程结束重写后,会向主进程发送一个信号,主进程接收到之后会调用信号处理函数执行以下步骤:

  1. 将 AOF 重写缓冲区内容写入新的 AOF 文件中。此时新文件所保存的数据库状态就和当前数据库状态一致了
  2. 对新文件进行改名,原子地覆盖现有 AOF 文件,完成新旧文件的替换。

当函数执行完成后,主进程就继续处理客户端命令。

因此,在整个 AOF 重写过程中,只有在执行信号处理函数时才会阻塞主进程,其他时候都不会阻塞。

3. 选择持久化方案的官方建议

到目前为止,Redis 的两种持久化方式就介绍得差不多了。可能你会有疑惑,在实际项目中,我到底要选择哪种持久化方案呢?下面,我贴下官方建议:

通常,如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么你可以仅使用 RDB。

很多用户仅使用了 AOF,但是我们建议,既然 RDB 可以时不时的给数据做个完整的快照,并且提供更快的重启,所以最好还是也使用 RDB。

在数据恢复方面:
RDB 的启动时间会更短,原因有两个:

  1. RDB 文件中每一条数据只有一条记录,不会像 AOF 日志那样可能有一条数据的多次操作记录。所以每条数据只需要写一次就行了。
  2. RDB 文件的存储格式和 Redis 数据在内存中的编码格式是一致的,不需要再进行数据编码工作,所以在 CPU 消耗上要远小于 AOF 日志的加载。

注意:

上面说了 RDB 快照的持久化,需要注意:在进行快照的时候(save),fork 出来进行 dump 操作的子进程会占用与父进程一样的内存,真正的 copy-on-write,对性能的影响和内存的耗用都是比较大的。比如机器 8G 内存,Redis 已经使用了 6G 内存,这时 save 的话会再生成 6G,变成 12G,大于系统的 8G。这时候会发生交换;要是虚拟内存不够则会崩溃,导致数据丢失。所以在用 redis 的时候一定对系统内存做好容量规划。

目前,通常的设计思路是利用复制(Replication)机制来弥补 aof、snapshot 性能上的不足,达到了数据可持久化。即 Master 上 Snapshot 和 AOF 都不做,来保证 Master 的读写性能,而 Slave 上则同时开启 Snapshot 和 AOF 来进行持久化,保证数据的安全性。

总结

文章知识点有点多和杂,我总结一下,帮助他们回顾内容:

  • RDB 持久化是 Redis 默认持久化方式,通过保存数据库键值对来记录状态来持久化,由 SAVE 和 BGSAVE 命令来创建 RDB 文件。前者阻塞 Redis 主进程,后者不会。
  • RDB 可以在配置文件设置每隔多久时间来执行 BGSAVE 命令
  • AOF 通过追加写命令来保存当前数据库状态。其持久化功能的实现可以分为 3 个步骤:命令追加(到 AOF 缓冲区)、文件写入(缓冲区内容写到 AOF 文件)、文件同步(AOF 文件保存磁盘)
  • 其中文件同步和保存可以通过配置文件的 appendfsync 选项来决定
    为了解决 AOF 文件越来越大的问题,Redis 提供了 AOF 重写功能,并且不会阻塞主进程。
  • 为了解决 AOF 重写过程中,新 AOF 文件所保存的数据库状态和当前数据库状态可能不一致的问题,Redis 引入了 AOF 重写缓冲区,用于保存子进程在重写 AOF 文件期间产生的新的写命令。
  • 最后是官方对于两种持久化方式选择的一些建议

openresty+redis拦截高频访问IP

CC攻击

网站受到攻击通常是黑客通过几个甚至多个IP地址,在短时间内进行超高频率访问,从而让服务器在短时间内增加巨大的计算量,导致负载增加,降低响应能力,甚至直接宕机停止服务。
通常这类情况我们只能通过查看分析网站日志,从而获得攻击者的IP地址,再通过防火墙进行拦截。
但一般而言这只会发生在监控系统已经开始报警之后,也就是网站或服务已经遭受到了攻击,并造成影响之后。并且在日志中搜寻到攻击者的IP并不是十分简单的事情,通常当我们找到了攻击者,攻击者可能已经停止了攻击,我们也只能预防他下次可能的攻击。

自动拦截

经历了黑客们深夜的骚扰和攻击,如何让那些短时间内大量访问的地址被自动拦截变成了努力的方向。
云服务商提供了WAF等商业化产品,协助我们处理这些威胁。
相比较于这些高昂价格的产品,开源软件同样在灵活性和可整合性上有很大的优势,接下来就介绍一下我是如何使用openresty和redis实现拦截高频访问的地址。

安装环境

之前的文章已经介绍过:Openresty+Redis 动态切换upstream (http://learn-learn.top/archives/169.html)
大致按照官方介绍就可以轻松安装。

nginx配置

nginx在初始化时建立一个redis的链接,并且在每次访问前需要执行block.lua进行验证

init_by_lua_block {
    redis = require "redis"
    client = redis.connect('127.0.0.1', 6379)
}
server {
    listen 8080;
    location  / {
        access_by_lua_file /usr/local/nginx/conf/lua/block.lua; 
        proxy_pass http://192.168.1.102:8000;
    }
}

lua脚本:

function isConnected()
    return client:ping()
end
function createRedisConnection()
        return redis.connect('127.0.0.1', 6379)
end

if pcall(isConnected)then --如果发生redis连接失败,将停止拦截。
    --
else
    if pcall(createRedisConnection)then     --断开重连会发送每次访问都需要重连redis
        client = createRedisConnection();       --如果访问量大的情况下,建议关闭重连,if pcall不执行,直接ngx.exit
    else
        ngx.exit(ngx.OK);
    end 
end


local ttl = 60;     --监测周期
local bktimes = 30; --在监测周期内达到触发拦截的访问量
block_ttl = 600;    --触发拦截后拦截时间
ip = ngx.var.remote_addr
ipvtimes = client:get(ip)

if(ipvtimes)then
    if(ipvtimes == "-1")then
        --ngx.say("blocked")
        return ngx.exit(403);
    else
        last_ttl = client:ttl(ip)
        --ngx.say("key exist.ttl is ",last_ttl);
        if(last_ttl==-1)then
            client:set(ip,0)
            client:expire(ip,ttl)
            --ngx.say("ttl & vtimes recount")
            return ngx.exit(ngx.OK);
        end
        vtimes = tonumber(client:get(ip))+1;
        if(vtimes<bktimes)then
            client:set(ip,vtimes);
            client:expire(ip,last_ttl)
            --ngx.say(ip," view ",vtimes," times");
            return ngx.exit(ngx.OK);
        else
            --ngx.say(ip," will be block noext time.")
            client:set(ip,-1);
            client:expire(ip,block_ttl)
            return ngx.exit(ngx.OK);
        end
    end
else
    --ngx.say("key do not exist")
    client:set(ip,1)
    --ngx.say(ip," view 1 times")
    client:expire(ip,ttl)
    return ngx.exit(ngx.OK)
end

脚本说明:

1.重要参数:

ttl = 60; –监测周期
bktimes = 30; –在监测周期内达到触发拦截的访问量
block_ttl = 600; –触发拦截后拦截时间

以上参数表示,一个IP地址在60秒内访问超过30次将被拦截600秒。

2.逻辑说明:

a)检测初始化的redis连接是否能够正常运行,如果连接失败或已经断开,将会重新建立连接,如果仍旧无法连接,将直接放行。这里是为了避免redis宕机导致nginx无法正常响应。当然如果初始连接中断,将会导致每次访问都会创建redis连接。

b)当某个IP首次访问时,将在redis中新建一个以IP地址为KEY的键(如果需要多个站点,修改下key的命名规则即可),value为1,并设置expire时间。当这个地址再次访问且key尚未过期前,将会每次递增key的value数,直到到达达到bktimes,或者key到期从而消亡。(本人用的redis5.0,到期key会直接不存在,可能部分版本到期后value为-1)

c)当key过期后,对于系统而言就是第一次访问,重新创建value为1的新key

d)当达到bktimes后,会将对应的IP的key的value设置为-1,且过期时间为block_ttl。

e)当访问到value为-1的key,即某个IP达到了我们设定的访问频次,我们将直接拦截,返回403.

3.完善方向:

a)在访问前添加黑白名单功能(这个在redis中新建立两个key即可)
b)拦截IP段(根据访问的IP地址建立以IP段为key的字段即可)
c)redis断开重连后,每次访问都要建立连接问题。

Win10下使用Docker运行redis

在windows下安装一些服务器的开发组件并不是很方便,通常都会用到虚拟机。

Docker 是个好东西,有现成的镜像直接可以使用。不用费时费力在windows上折腾服务器需要的环境。

只要几步就能搞定,非常方便

  1. 注册账号,安装Docker
  2. 获取Redis
  3. 运行Redis
    OK

安装Docker

1、注册一个Docker账号
2、下载并安装Docker

官网下载Docker: https://www.docker.com/get-started

安装完成后,在托盘里有个小鲸鱼图标。在命令行cmd下输入 docker version,可以看到相关信息

D:>docker version
Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        e68fc7a
 Built:             Tue Aug 21 17:21:34 2018
 OS/Arch:           windows/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       e68fc7a
  Built:            Tue Aug 21 17:29:02 2018
  OS/Arch:          linux/amd64
  Experimental:     false

获取Redis

运行命令行(cmd)工具,直接下载 redis镜像。

d:> docker pull redis

从服务器上拉取redis镜像包,可以使用docker images查看现有的镜像。

D:>docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
redis                      latest              e1a73233e3be        5 weeks ago         83.4MB

运行Redis 并进行端口映射和持久化

一条语句就搞定

docker run -d --name myredis -p6379:6379 -v /d/dockerdata/redis/data:/data redis-server --appendonly yes

参数说明:

  • -d —— 后台运行
  • –name —— 实例运行后的名字 myredis
  • -p6379:6379 —— 端口映射,冒号前面是windows下的端口,后面是虚拟机的端口
  • -v /d/dockerdata/redis/data:/data —— 保存数据的位置。

  • d:dockerdataredisdata 前面是windows下的实际保存数据目录

  • /data 虚拟机内的目录

  • redis-server –appendonly yes —— 在容器执行redis-server启动命令,并打开redis持久化配置。

第一次映射时,会提示需要输入windows的密码,直接输入即可。

查看运行状态和执行命令

执行完成后,可以使用docker ps查看运行状态

D:>docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
443ea1afa978        redis               "docker-entrypoint.s…"   About an hour ago   Up About an hour    0.0.0.0:6379->6379/tcp   myredis

可以看到myredis在运行。

在docker下运行redis客户端,就能使用命令来测试

docker exec -it myredis redis-cli

D:>docker exec -it myredis redis-cli
127.0.0.1:6379> info
# Server
redis_version:4.0.11
redis_git_sha1:00000000
redis_git_dirty:0
... ...

重新恢复启动容器

电脑关闭重启或重启docker后,如果没有设置自动启动容器。通过docker ps -a可以看到所有关闭的容器,启动容器。

docker ps -a #看到关闭的容器
docker start your_container_name
docker attach your_container_name

如启动刚才的myredis

D:>docker start myredis
myredis

D:>docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
443ea1afa978        redis               "docker-entrypoint.s…"   2 hours ago         Up 5 seconds        0.0.0.0:6379->6379/tcp   myredis

OK 搞定

Docker是个非常棒的东东,解决了不同环境下大量的部署工作。同样方法非常方便的安装其他镜像,如debian、mysql等等。如想安装mysql,使用docker的search命令就能找到一堆。STARS越高使用的人越多,相对比较安全。

D:>docker search mysql
NAME                                                   DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
mysql                                                  MySQL is a widely used, open-source relation…   7119                [OK]
mariadb                                                MariaDB is a community-developed fork of MyS…   2284                [OK]
mysql/mysql-server                                     Optimized MySQL Server Docker images. Create…   521                                     [OK]
zabbix/zabbix-server-mysql                             Zabbix Server with MySQL database support       133                                     [OK]
hypriot/rpi-mysql                                      RPi-compatible Docker Image with Mysql          98

Linux搭建ELK日志收集系统:FIlebeat+Redis+Logstash+Elasticse

Centos7部署ELK日志收集系统

一、ELK概述

ELK是一组开源软件的简称,其包括Elasticsearch、Logstash 和 Kibana。ELK最近几年发展迅速,已经成为目前最流行的集中式日志解决方案。

  • Elasticsearch: 能对大容量的数据进行接近实时的存储,搜索和分析操作。 本项目中主要通过Elasticsearch存储所有获取的日志。
  • Logstash: 数据收集引擎,它支持动态的的从各种数据源获取数据,并对数据进行过滤,分析,丰富,统一格式等操作,然后存储到用户指定的位置。
  • Kibana: 数据分析与可视化平台,对Elasticsearch存储的数据进行可视化分析,通过表格的形式展现出来。
  • Filebeat: 轻量级的开源日志文件数据搜集器。通常在需要采集数据的客户端安装Filebeat,并指定目录与日志格式,Filebeat就能快速收集数据,并发送给logstash进行解析,或是直接发给Elasticsearch存储。
  • Redis:NoSQL数据库(key-value),也数据轻型消息队列,不仅可以对高并发日志进行削峰还可以对整个架构进行解耦

传统ELK的经典框架

未分类

单一的架构,logstash作为日志搜集器,从数据源采集数据,并对数据进行过滤,格式化处理,然后交由Elasticsearch存储,kibana对日志进行可视化处理。

新型ELK框架

未分类

Filebeats是一种轻量级的日志搜集器,其不占用系统资源,自出现之后,迅速更新了原有的elk架构。Filebeats将收集到的数据发送给Logstash解析过滤,在Filebeats与Logstash传输数据的过程中,为了安全性,可以通过ssl认证来加强安全性。之后将其发送到Elasticsearch存储,并由kibana可视化分析。

二、新型ELK搭建详细过程

实验环境:

未分类

下面是搭建过程中所需程序安装包:
https://pan.baidu.com/s/1w02WtUAqh9yX4TChyMLa5Q 密码:g0p9

1.客户端部署filebeat:

yum -y install filebeat
#查看配置文件所在位置
rpm -qc filebeat

2.修改配置文件使filebeat获取的日志进入redis:

注:此处演示获取spring cloud框架中eureka日志,其他程序日志都可相同方法获取

vim /etc/filebeat/filebeat.yml
#修改的内容有一家几个字段
enabled:true
paths:程序日志路径
output.redis:日志输出地方
                    hosts:redis所在服务器IP
                    port:redis端口
                    key:redis中的key

未分类

3.源码安装redis:

解压redis程序包:

tar zxf redis-3.2.9.tar.gz –C /usr/local/src

编译redis:

cd /usr/local/src/redis-3.2.9
make && make install
ln –s /usr/local/src/redis-3.2.9 /usr/local/redis

注:redis安装时有的缺少语言环境会出错,有的会出现奇奇怪怪的问题,只要复制Error到往上搜索下就能轻易解决,在此不多做解释

修改redis配置文件:

vim /usr/local/redis/redis.conf
#修改内容如下:
daemonize yes                           #开启后台运行
timeout 120                                #超时时间
bind 0.0.0.0                                #任何地址IP都可以登录redis
protected-mode no                     #关闭redis保护机制否则在没有密码校验情况下redis远程登录失败

注:此处是做演示,如果是线上部署elk建议开启持久化机制,保证数据不丢失

4.登录测试redis是否可以正常写入数据:

未分类

5.启动filebeat看看redis是否能接收到数据:

启动filebeat:

systemctl start filebeat

6.进入redis查看是否有数据:

#执行命令:
keys *                          #查看所有key,此操作为慢查询,若redis跑了大量线上业务请不要进行此操做
lrange eureka-log 0 -1 #查询key所有数据,若filebeat启动时间过长请勿进行此操作

未分类

7.安装jdk1.8:

解压jdk安装包并创建软连接:

tar zxf /usr/local/src/jdk-8u131-linux-x64.tar.gz –C /usr/local/
ln -s /usr/local/jdk1.8.0_91/ /usr/local/jdk

配置环境变量:

vim /etc/profile
#修改内容如下:
JAVA_HOME=/usr/local/jdk
export JRE_HOME=/usr/local/jdk/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin

重新载入环境变量:

source /etc/profile

查看jdk是否安装成功:

java -version

未分类

8.安装Elasticsearch:

解压安装包并改名:

unzip elasticsearch-5.6.3.zip -d /usr/local/
mv /usr/local/ elasticsearch-5.6.3 /usr/local/elasticsearh

修改ES配置文件:

vim /usr/local/elasticsearch/config/elasticsearch.yml

#这里指定的是集群名称,需要修改为对应的,开启了自发现功能后,ES会按照此集群名称进行集群发现
cluster.name: my-application
node.name: node-1

#目录需要手动创建
path.data: /opt/elk/data
path.logs: /opt/elk/logs

#ES监听地址任意IP都可访问
network.host: 0.0.0.0
http.port: 9200

#若是集群,可在里面引号中添加,逗号隔开
discovery.zen.ping.unicast.hosts: [“192.168.3.205”]

# enable cors,保证_site类的插件可以访问es    
http.cors.enabled: true             #手动添加
http.cors.allow-origin: “*”         #手动添加

# Centos6不支持SecComp,而ES5.2.0默认bootstrap.system_call_filter为true进行检测,所以导致检测失败,失败后直接导致ES不能启动
bootstrap.memory_lock: false        #手动添加
bootstrap.system_call_filter: false     #手动添加

注:ES启动的时候回占用特别大的资源所以需要修改下系统参数,若不修改资源启动会异常退出

9.修改系统参数:

vim /etc/sysctl.conf
#添加参数
vm.max_map_count=655360

重新载入配置:

sysctl –p

10.修改资源参数:

vim /etc/security/limits.conf
#修改

*   soft    nofile  65536
*   hard        nofile  131072  
*   soft        nproc   65536
*   hard        nproc   131072 

如:

未分类

11.设置用户资源参数:

vim /etc/security/limits.d/20-nproc.conf
#添加
elk     soft    nproc       65536

12.创建用户并赋权:

useradd elk
groupadd elk
useradd elk -g elk

13.创建数据和日志目录并修改目录权限:

mkdir –pv /opt/elk/{data,logs}
chown –R elk:elk /opt/elk
chown –R elk:elk /usr/local/elasticsearch

14.切换用户并后台启动ES:(elk用户修改了资源参数,如不切位elk用户启动会暴毙)

su elk
nohup /opt/app/elasticsearch-5.6.3/bin/elasticsearch >> /dev/null 2>&1 &

15.查看ES状况:

方法一、
curl 'http://[ES IP]:9200/_search?pretty'

方法二、
#网页访问:
http://[ES IP]:9200/_search?pretty

16.安装logstash:

解压并创建软连接:

tar /usr/local/src/logstash-5.3.1.tar.gz –C /usr/local/
ln –s /usr/local/logstash-5.3.1 /usr/local/logstash

测试logstash是否可用:

/usr/local/logstash/bin/logstash -e 'input { stdin { } } output { stdout {} }'

未分类

在此创建主配文件进行测试:

vim /usr/local/logstash/config/logstash-simple.conf
#内容如下:
input { stdin { } }
output {
    stdout { codec=> rubydebug }
}

使用logstash参数-f读取配置文件进行测试:

/usr/local/logstash/bin/logstash -f /usr/local/logstash/config/logstash-simple.conf

未分类

此时说明我们的logstash是完全没有问题了,可以进行日志收集了

17.创建配置文件获取redis日志的数据:

配置文件如下:

vim /usr/local/logstash/config/redis-spring.conf 
input {
  redis {
    port => "6379"
    host => "192.168.3.205"
    data_type => "list"
    type => "log"
    key => "eureka-log"
  }
}
output {
  elasticsearch {
     hosts => "192.168.3.205:9200"
     index => "logstash1-%{+YYYY.MM.dd}"
  }
}

通过配置文件启动服务查看效果:

/usr/local/logstash/bin/logstash -f /usr/local/logstash/config/redis-spring.conf

结果如下:

未分类

此时我们再去查看reids中key:(此时已经没有数据了,数据已经被logstash取完)

未分类

18.使用curl 查看ES是否接受到数据

curl http://192.168.3.205:9200/_search?pretty

结果如下:

未分类

此时说明我们logstash从redis中取数据,在把数据推到ES中是ok的!

19.安装ES插件:(elasticsearch-head)

注:head安装需要从国外网站拉去东西,可能网速过慢导致安装失败(可以多试几次),下面有几种方法安装:

方法一、
导入node-v8.2.1.tar.gz phantomjs-2.1.1-linux-x86_64.tar.bz2 安装包
安装node:
tar zxvf node-v8.2.1.tar.gz
cd node-v8.2.1/
./configure && make && make install 

安装phantomjs:
tar jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
cd phantomjs-2.1.1-linux-x86_64/bin/
cp phantomjs /usr/local/bin/

导入es-head程序包并解压:
unzip master.zip –d /usr/local/
cd elasticsearch-head/
npm install
npm run start &

查看端口状态:(端口默认9100)
netstat –anpt | grep 9100

方法二、
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
netstat –anpt | grep 9100

方法三、
拉镜像:
docker push mobz/elasticsearch-head:5
启动镜像:
docker run -p 9100:9100 mobz/elasticsearch-head:5
web访问测试:
http://IP:9100

20.Elasticsearch-head安装成功Web访问结果如下:

未分类

查看刚刚从logstash推到ES中的数据:

未分类

21.安装kibana

解压并安装kibana:

tar -zxvf /usr/local/src/kibana-5.3.1-linux-x86_64.tar.gz -C /usr/local/

修改kibana配置文件:

vim /usr/local/kibana-5.3.1-linux-x86_64/config/kibana.yml

修改内容如下:

server.port: 5601                                                            #开启默认端口5601
server.host: “192.168.3.205”                                    #kibana站点IP
elasticsearch.url: http://192.168.3.205:9200        #只想ES服务所在IP Port
kibana.index: “.kibana”

后台启动kibana:

nohup /usr/local/kibana-5.3.1-linux-x86_64/bin/kibana >> /dev/null 2>&1 &

查看端口监听:

netstat –anot | grep 5601

结果如:(此结果表示kibana启动成功)

未分类

使用Web访问kibana:

http://[Kibana IP]:5601

初次访问结果如:(刚访问的时候没有创建索引所以没有看不到数据)

未分类

根据logstash配置文件中index设置索引:
首先查看logstash中的index:

未分类

Kibana中创建index:

未分类

下面按照1,2,3,4顺序进行设置:

未分类

此时我们在返回Discover在里面我们就可以看到数据了:

未分类

至此我们的ELK就安装OK了。

Redis 与 fastjson 实现存储与读取

1、数组 list

Redis 结合 fastjson 存储

List<Home> home = new ArrayList<Home>();
String key = "redisKey";
redisUtil.set(key, JSON.toJSONString(home));

Redis 结合 fastjson 读取

String key = "redisKey";    
Object value = redisUtil.get(key);  
List<Home> home =  JSON.parseArray((String)value, Home.class);

2、HashMap

Redis 结合 fastjson 存储

Map<Integer, List<ItemsToTemplate>> itemMap = new HashMap<Integer, List<ItemsToTemplate>>();
String key = "redisKey";
redisUtil.set(key, JSON.toJSONString(itemMap));

Redis 结合 fastjson 读取

String key = "redisKey";
String value = redisUtil.getStr(key);   
Map<Integer, List<ItemsToTemplate>> itemMap = JSON.parseObject(value,new TypeReference<Map<Integer,List<ItemsToTemplate>>>(){}.getType());
B3log  Redis  Java 

基于Redis实现分布式锁

背景

在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试
SETNX foo.lock <current unix time>

如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过
DEL foo.lock

命令来释放锁。
如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。理想是美好的,现实是残酷的。仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

处理死锁

在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
C2 向foo.lock发送DEL命令。
C2 向foo.lock发送SETNX获取锁。
C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
C3 向foo.lock发送SETNX获取锁。

此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,看看如何使用GETSET方式,避免这种情况产生。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时。
C4 向foo.lock发送GESET命令,
GETSET foo.lock
并得到foo.lock中老的时间戳T2

如果T1=T2,说明C4获得时间戳。
如果T1!=T2,说明C4之前有另外一个客户端C5通过调用GETSET方式获取了时间戳,C4未获得锁。只能sleep下,进入下次循环中。

现在唯一的问题是,C4设置foo.lock的新时间戳,是否会对锁产生影响。其实我们可以看到C4和C5执行的时间差值极小,并且写入foo.lock中的都是有效时间错,所以对锁并没有影响。
为了让这个锁更加强壮,获取锁的客户端,应该在调用关键业务时,再次调用GET方法获取T1,和写入的T0时间戳进行对比,以免锁因其他情况被执行DEL意外解开而不知。以上步骤和情况,很容易从其他参考资料中看到。客户端处理和失败的情况非常复杂,不仅仅是崩溃这么简单,还可能是客户端因为某些操作被阻塞了相当长时间,紧接着 DEL 命令被尝试执行(但这时锁却在另外的客户端手上)。也可能因为处理不当,导致死锁。还有可能因为sleep设置不合理,导致Redis在大并发下被压垮。最为常见的问题还有

GET返回nil时应该走那种逻辑?

第一种走超时逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 通过T0>T1+expire对比,进入GETSET流程。
C2 调用GETSET向foo.lock发送T0时间戳,返回foo.lock的原值T2
C2 如果T2=T1相等,获得锁,如果T2!=T1,未获得锁。

第二种情况走循环走setnx逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 循环,进入下一次SETNX逻辑

两种逻辑貌似都是OK,但是从逻辑处理上来说,第一种情况存在问题。当GET返回nil表示,锁是被删除的,而不是超时,应该走SETNX逻辑加锁。走第一种情况的问题是,正常的加锁逻辑应该走SETNX,而现在当锁被解除后,走的是GETST,如果判断条件不当,就会引起死锁,很悲催,我在做的时候就碰到了,具体怎么碰到的看下面的问题

GETSET返回nil时应该怎么处理?

C1和C2客户端调用GET接口,C1返回T1,此时C3网络情况更好,快速进入获取锁,并执行DEL删除锁,C2返回T2(nil),C1和C2都进入超时处理逻辑。
C1 向foo.lock发送GETSET命令,获取返回值T11(nil)。
C1 比对C1和C11发现两者不同,处理逻辑认为未获取锁。
C2 向foo.lock发送GETSET命令,获取返回值T22(C1写入的时间戳)。
C2 比对C2和C22发现两者不同,处理逻辑认为未获取锁。

此时C1和C2都认为未获取锁,其实C1是已经获取锁了,但是他的处理逻辑没有考虑GETSET返回nil的情况,只是单纯的用GET和GETSET值就行对比,至于为什么会出现这种情况?一种是多客户端时,每个客户端连接Redis的后,发出的命令并不是连续的,导致从单客户端看到的好像连续的命令,到Redis server后,这两条命令之间可能已经插入大量的其他客户端发出的命令,比如DEL,SETNX等。第二种情况,多客户端之间时间不同步,或者不是严格意义的同步。

时间戳的问题

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。
锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

分布式锁的问题

1:必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。
2:分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在锁串的情况。要适度的机制,可以承受小概率的事件产生。
3:只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,直接进行操作,然后释放,尽量减少持有锁的时间。
4:在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,玩客没有选择做锁的检查。
5:sleep学问,为了减少对Redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。需要根据自己的Redis的QPS,加上持锁处理时间等进行合理计算。
6:至于为什么不使用Redis的muti,expire,watch等机制,可以查一参考资料,找下原因。

锁测试数据

未使用sleep

第一种,锁重试时未做sleep。单次请求,加锁,执行,解锁时间

未分类

可以看到加锁和解锁时间都很快,当我们使用

ab -n1000 -c100 'http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t'
AB 并发100累计1000次请求,对这个方法进行压测时。

未分类

我们会发现,获取锁的时间变成,同时持有锁后,执行时间也变成,而delete锁的时间,将近10ms时间,为什么会这样?
1:持有锁后,我们的执行逻辑中包含了再次调用Redis操作,在大并发情况下,Redis执行明显变慢。
2:锁的删除时间变长,从之前的0.2ms,变成9.8ms,性能下降近50倍。
在这种情况下,我们压测的QPS为49,最终发现QPS和压测总量有关,当我们并发100总共100次请求时,QPS得到110多。当我们使用sleep时

使用Sleep时

单次执行请求时

未分类

我们看到,和不使用sleep机制时,性能相当。当时用相同的压测条件进行压缩时

未分类

获取锁的时间明显变长,而锁的释放时间明显变短,仅是不采用sleep机制的一半。当然执行时间变成就是因为,我们在执行过程中,重新创建数据库连接,导致时间变长的。同时我们可以对比下Redis的命令执行压力情况

未分类

上图中细高部分是为未采用sleep机制的时的压测图,矮胖部分为采用sleep机制的压测图,通上图看到压力减少50%左右,当然,sleep这种方式还有个缺点QPS下降明显,在我们的压测条件下,仅为35,并且有部分请求出现超时情况。不过综合各种情况后,我们还是决定采用sleep机制,主要是为了防止在大并发情况下把Redis压垮,很不行,我们之前碰到过,所以肯定会采用sleep机制。

Redis查漏补缺:最易错过的技术要点大扫盲

考虑到绝大部分写业务的程序员在实际开发中使用Redis时,只会Setvalue和Getvalue两个操作,对Redis整体缺乏一个认知。又恰逢笔者有同事下周要去培训Redis,所以笔者斗胆以Redis为主题,对Redis常见问题做一个总结,希望能够扫除大家的知识盲点。

本文围绕以下几点进行阐述:

  • 为什么使用Redis
  • 使用Redis有什么缺点
  • 单线程的Redis为什么这么快
  • Redis的数据类型,以及每种数据类型的使用场景
  • Redis的过期策略以及内存淘汰机制
  • Redis和数据库双写一致性问题
  • 如何应对缓存穿透和缓存雪崩问题
  • 如何解决Redis的并发竞争问题

一、为什么使用Redis

笔者认为,在项目中使用Redis,主要是从两个角度去考虑:性能和并发。当然,Redis还具备可做分布式锁等功能的其它功能,但如果只是为了分布式锁这些其它功能,完全还有其它中间件(如Zookpeer等)可以代替,并不是非要使用Redis。

因此,这个问题主要从性能和并发两个角度去答:

1、性能

如下图所示,我们在碰到需要执行耗时特别久、且结果不频繁变动的SQL时,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

未分类

题外话:忽然想聊一下这个迅速响应的标准——其实根据交互效果的不同,这个响应时间没有固定标准。不过曾经有人这么告诉我:“在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。”

那么瞬间、刹那、一弹指具体是多少时间呢?

根据《摩诃僧祗律》记载:一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。

那么,经过周密的计算,一瞬间为0.36秒,一刹那有0.018秒,一弹指长达7.2秒。

2、并发

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。

未分类

二、使用Redis有什么缺点

大家用Redis这么久,这个问题是必须要了解的,基本上使用Redis都会碰到一些问题,常见的主要是四方面的问题:

  • 缓存和数据库双写一致性问题
  • 缓存雪崩问题
  • 缓存击穿问题
  • 缓存的并发竞争问题

这四个问题,笔者个人觉得在项目中比较常遇见,具体解决方案,后文会给出。

三、单线程的Redis为什么这么快

这个问题其实是对Redis内部机制的一个考察。其实根据笔者的面试经验,很多人其实都不知道Redis是单线程工作模型。所以,这个问题还是应该要复习一下的。主要是以下三点:

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞I/O多路复用机制

我们现在仔细地说一说I/O多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么意思。打一个比方:小曲在S城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。

经营方式一:
客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在很多问题,几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。

随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了,快递员之间的协调很花时间,大部分时间花在抢车上。综合上述缺点,小曲痛定思痛,提出了下面的经营方式↓

经营方式二:
小曲只雇佣一个快递员,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次去取快递,一次拿一个,开着车去送快递,送好了就回来拿下一个快递。

上述两种经营方式对比,是不是明显觉得第二种,效率更高、更好呢?在上述比喻中:

  • 每个快递员→每个线程
  • 每个快递→每个Socket(I/O流)
  • 快递的送达地点→Socket的不同状态
  • 客户送快递请求→来自客户端的请求
  • 小曲的经营方式→服务端运行的代码
  • 一辆车→CPU的核数

于是我们有如下结论:

  • 经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。
  • 经营方式二就是I/O多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。

下面类比到真实的Redis线程模型,如图所示:

未分类

参照上图,简单来说就是,我们的Redis-client在操作的时候,会产生具有不同事件类型的Socket。在服务端,有一段I/O多路复用程序,将其置入队列之中。然后文件事件分派器依次去队列中取,转发到不同的事件处理器中。

需要说明的是,这个I/O多路复用机制,Redis还提供了Select、Epoll、Evport、Kqueue等多路复用函数库,大家可以自行去了解。

四、Redis的数据类型及各自使用场景

看到这个问题,是不是觉得它很基础?其实笔者也这么觉得。然而根据面试经验发现,至少80%的人答不上这个问题。建议在项目中用到后,再类比记忆,体会更深,不要硬记。基本上,一个合格的程序员五种类型都会用到:

1、String

这个其实没什么好说的,最常规的Set/Get操作,Value可以是String也可以是数字,一般做一些复杂的计数功能的缓存。

2、Hash

这里Value存放的是结构化的对象,比较方便的就是操作其中的某个字段。笔者在做单点登录的时候,就是用这种数据结构存储用户信息,以CookieId作为Key,设置30分钟为缓存过期时间,能很好地模拟出类似Session的效果。

3、List

使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用Lrange命令,做基于Redis的分页功能,性能极佳,用户体验好。

4、Set

因为Set堆放的是一堆不重复值的集合,所以可以做全局去重的功能。

为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set比较麻烦,难道为了做一个全局去重,再起一个公共服务?太麻烦了。

另外,就是利用交集、并集、差集等操作,可以计算共同喜好、全部的喜好、自己独有的喜好等功能。

5、Sorted Set

Sorted Set多了一个权重参数Score,集合中的元素能够按Score进行排列。可以做排行榜应用,取TOP N操作。另外,Sorted Set还可以用来做延时任务。最后一个应用就是可以做范围查找。

五、Redis的过期策略及内存淘汰机制

这个问题其实相当重要,从这个问题就可以看出来到底Redis有没有用到位。比如,你Redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的?这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

Redis采用的是定期删除+惰性删除策略。

为什么不用定时删除策略?

定时删除,用一个定时器来负责监视Key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除Key,因此没有采用这一策略。

定期删除+惰性删除是如何工作的呢?

定期删除,Redis默认每个100ms检查是否有过期的Key,有过期Key则删除。需要说明的是,Redis不是每个100ms将所有的Key检查一次,而是随机抽取进行检查(如果每隔100ms,全部Key进行检查,Redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多Key到时间没有删除。

于是,惰性删除派上用场。也就是说在你获取某个Key的时候,Redis会检查一下,这个Key如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除Key。然后你也没及时去请求Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高,那么就应该采用内存淘汰机制。

在Redis.conf中有一行配置:

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的:

  • Noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人使用吧;
  • Allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的Key。推荐使用,目前项目在用这种;
  • Allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,应该也没人使用吧;
  • Volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的Key。这种情况一般是把Redis既当缓存又做持久化存储的时候才用。不推荐;
  • Volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个Key。依然不推荐;
  • Volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的Key优先移除。不推荐。

PS:如果没有设置Expire的Key,不满足先决条件(Prerequisites);那么Volatile-lru、Volatile-random和Volatile-ttl策略的行为,和Noeviction(不删除)基本上一致。

六、Redis和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题,想要回答这个问题,就要先明白一个前提:如果对数据有强一致性要求,就不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据不能放缓存。

《分布式数据库与缓存双写一致性方案解疑》 https://mp.weixin.qq.com/s?__biz=MzI4NTA1MDEwNg==&mid=2650767895&idx=1&sn=eb87586d2b7748021fd8cd1791d5d39e&chksm=f3f93782c48ebe94ec0e85bc9e08ac1b478614ca500128a7f42b6dee60c20bc4ade86ce044be&scene=21#wechat_redirect 给出了详细的分析,在这里简单地说一说:首先,采取正确更新策略,先更新数据库,再删缓存;其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

七、应对缓存穿透和缓存雪崩问题

关于“如何应对缓存穿透和缓存雪崩”这两个问题,说句实在话,一般中小型传统软件企业很难碰到。如果有大并发的项目,流量有几百万左右,这两个问题一定要深刻考虑:

1、应对缓存穿透

缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

解决方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库,没得到锁,则休眠一段时间重试;
  • 采用异步更新策略,无论Key是否取到值,都直接返回。Value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存,需要做缓存预热(项目启动前,先加载缓存)操作;
  • 提供一个能迅速判断请求是否有效的拦截机制,比如利用布隆过滤器,内部维护一系列合法有效的Key,迅速判断出,请求所携带的Key是否合法有效,如果不合法,则直接返回。

2、应对缓存雪崩

缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

解决方案:

  • 给缓存的失效时间加上一个随机值,避免集体失效;
  • 使用互斥锁,但是该方案吞吐量明显下降了;
  • 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间,自己做缓存预热操作。然后细分以下几个小点:
    a. 从缓存A读数据库,有则直接返回;
    b. A 没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程;
    c. 更新线程同时更新缓存A和缓存B。

八、如何解决Redis并发竞争Key问题

这个问题大致就是同时有多个子系统去Set一个Key。这个时候要注意什么呢?本人提前百度了一下,发现大家思考的答案基本都是推荐用Redis事务机制。但本人不推荐使用Redis的事务机制。因为我们的生产环境,基本都是Redis集群环境,做了数据分片操作。你一个事务中有涉及到多个Key操作的时候,这多个Key不一定都存储在同一个Redis-Server上。因此,Redis的事务机制,十分鸡肋。

解决方法如下:

如果对这个Key操作不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做Set操作即可,比较简单。

如果对这个Key操作要求顺序

假设有一个Key1,系统A需要将Key1设置为ValueA,系统B需要将Key1设置为ValueB,系统C需要将Key1设置为ValueC。期望按照Key1的Value值按照 ValueA→ValueB→ValueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:

  • 系统A Key 1 {ValueA 3:00}
  • 系统B Key 1 {ValueB 3:05}
  • 系统C Key 1 {ValueC 3:10}

那么,假设这会系统B先抢到锁,将Key1设置为{ValueB 3:05}。接下来系统A抢到锁,发现自己的ValueA的时间戳早于缓存中的时间戳,那就不做Set操作了。以此类推。

其他方法,比如利用队列,将Set方法变成串行访问也可以。总之,灵活变通。

九、总结

本文对Redis的常见问题做了一个总结。大部分是笔者自己在工作中遇到,以及以前面试别人的时候常问的一些问题,希望大家能够有所收获。