【网页加速】lua redis的二次升级

之前发过openresty的相关文章,也是用于加速网页速度的,但是上次没有优化好代码,这次整理了下,优化了nginx的配置和lua的代码,感兴趣的话可以看看上篇的文章:
https://www.cnblogs.com/w1570631036/p/8449373.html

为了学习,不断的给自己的服务器装东西,又是logstash,又是kafka,导致主站网络负载、cpu消耗过大,再加上tomcat这个本身就特别占用内存的东西,只要稍微刷新一下网站,就能感受到蜗牛般的速度,实在受不了,前段时间给网站加了n多层缓存,依旧没有改观多少,想了想,算了,一直都这么卡,还不如直接将动态的网站直接变成静态网页存储在redis里面,然后关掉tomcat,貌似没有改观多少,但是在xshell里面敲命令没那么卡了,这里,也提出了一种别样的网站加速方法——redis存储静态网页。
未分类

一、总体流程如下

未分类
1.一次请求过来,通过openresty的nginx来访问lua脚本;
2.读取redis中是否存在该uri对应的静态网页,如果有,则直接返回,否则回源到tomcat,然后将响应的内容保存到redis里面。

二、nginx的设置

openresty中自带了nginx,所以只需要配置一下即可,我们最终的目前是拦截所有以html结尾的请求,如果是以其他后缀结尾的,比如do,则可以直接回滚到tomat里面去。
由于篇幅的关系,只粘贴部分nginx配置,想看全的请转至:mynginxconfig.ngx

server {
    listen       80;
    # listen       443 ssl;   # ssl
    server_name  www.wenzhihuai.com;
    location  ~ .*.(html)$ {       //拦截所有以html结尾的请求,调用lua脚本
        ...
        charset utf8;
        proxy_pass_request_headers off ;
        # 关闭缓存lua脚本,调试的时候专用
        lua_code_cache off;
        content_by_lua_file /opt/lua/hello.lua;
    }
    location / {        //nginx是按顺序匹配的,如果上面的不符合,那么将回滚tomcat
        default_type    text/html;
        root   html;
        index  index.html index.htm;
        ...
        # websocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://backend;
    }

三、lua脚本

为了方便key的操作,经过测试,即使uri带有各种字符,比如 ? . html = &等,都是可以直接设置为redis中的key的,所以,不是那么的需要考虑redis的key违反规则,可以直接将uri设置为key。具体流程如下:

local key = request_uri
首先,key为请求访问的uri
local resp, err = red:get(key)
去redis上查找有没有
if resp == ngx.null then
    如果没有
    ngx.req.set_header("Accept", "text/html,application/xhtml+xml,application/xml;")
    ngx.req.set_header("Accept-Encoding", "")
    这里,特别需要注意的是,要把头部的信息去掉,这里之前说过。(如果不去掉,就是gzip加密返回,然后再经过一层gzip加密返回给用户,导致用户看到的是gzip压缩过的乱码)
    local targetURL = string.gsub(uri, "html", "do")
    这里讲html替换为do,即:不拦截*.do的请求,其可以直接访问tomcat
    local respp = ngx.location.capture(targetURL, { method = ngx.HTTP_GET, args = uri_args })
    开始回源到tomcat
    red:set(key, respp.body)
    将uri(key)和响应的内容设到redis里面去
    red:expire(key, 600)
    lua redis并没有提供在set的时候同时设置过期时间,所以,额外加一行设置过期时间
    ngx.print(respp.body)
    将响应的内容输出给用户
    return
end
ngx.print(resp)

四、测试

进行一次测试,以访问http://www.wenzhihuai.com/jaowejoifjefoijoifaew.html 为例,我的网站并没有设置这个uri,所以,访问的时候,会统一调到错误页面,之后,会在redis中看到有这条记录:
未分类
该地址已经成功被缓存到redis里面去,点击其他页面,可以看到,只要是点击的页面,都被缓存到redis里面去了。总体来说,如果不设置过期时间,可以把整个网页静态化缓存到redis里面,甚至是可以关闭tomcat了,但是这种做法只适用于万年不变的页面,至于用于企业的话,,,,

后记:
其实我有个疑问,我的代码里,并没有设置lua断开redis的连接,不知道会不会有影响,而且它这个是指每次请求过来,都需要重新连接redis么?光是TCP三次握手就耗时不少啊,不知道怎么优化这些信息。

全部代码如下:

local redis = require "resty.redis"
local red = redis:new()
local request_uri = ngx.var.request_uri
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR

local function close_redis(red)
    if not red then
        return
    end
    local pool_max_idle_time = 10000
    local pool_size = 100
    red:set("pool_size", pool_size)
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx_log(ngx_ERR, "set redis keepalive error : ", err)
    end
end

local uri = ngx.var.uri

red:set_timeout(1000)
red:connect("119.23.46.71", 6340)
red:auth("root")
local uri_args = ngx.req.get_uri_args()

local key = request_uri
local resp, err = red:get(key)

if resp == ngx.null then
    ngx.req.set_header("Accept", "text/html,application/xhtml+xml,application/xml;")
    ngx.req.set_header("Accept-Encoding", "")
    local targetURL = string.gsub(uri, "html", "do")
    local respp = ngx.location.capture(targetURL, { method = ngx.HTTP_GET, args = uri_args })
    red:set(key, respp.body)
    red:expire(key, 600)
    ngx.print(respp.body)
    return
end
ngx.print(resp)
close_redis(red)

linux下源码安装redis

redis官方下载网址: https://redis.io/download

安装目录: /usr/local/bin/
配置文件路径: /etc/redis/redis.conf
配置端口: 6379
服务端: /usr/local/bin/redis-server
客户端: /usr/local/bin/redis-cli
持久化文件存放目录路径: /var/lib/redis
pid路径: /var/run/redis.pid
日志路径: /var/log/redis.log

1. centos下安装redis

以centos7.4为例.

1.1. 源码编译安装

$ mkdir ~/soft
$ cd ~/soft
$ wget -c http://download.redis.io/releases/redis-4.0.10.tar.gz
$ tar xzf redis-4.0.10.tar.gz
$ cd redis-4.0.10
$ make
$ sudo make install

1.2. 添加服务并添加到开机自启动

$ cd ~/soft/redis-4.0.10/utils
$ sudo ./install_server.sh
Welcome to the redis service installer
This script will help you easily set up a running redis server
Please select the redis port for this instance: [6379] 
Selecting default: 6379
Please select the redis config file name [/etc/redis/6379.conf] /etc/redis/redis.conf
Please select the redis log file name [/var/log/redis_6379.log] /var/log/redis.log
Please select the data directory for this instance [/var/lib/redis/6379] /var/lib/redis
Please select the redis executable path [] /usr/local/bin/redis-server
Selected config:
Port : 6379
Config file : /etc/redis/redis.conf
Log file : /var/log/redis.log
Data dir : /var/lib/redis
Executable : /usr/local/bin/redis-server
Cli Executable : /usr/local/bin/redis-cli
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6379.conf => /etc/init.d/redis_6379
Installing service...
Successfully added to chkconfig!
Successfully added to runlevels 345!
Starting Redis server...
Installation successful!
$ 

1.3. 创建redis用户及其用户组并给相应目录授权

$ sudo useradd --r --U -M redis
$ sudo chown redis:redis /var/lib/redis
$ sudo chmod 770 /var/lib/redis

1.4. 修改服务名及配置文件

重命名服务名redis_6379为redis, 在此之前先停止服务:

$ sudo /etc/init.d/redis_6379 stop
$ sudo mv /etc/init.d/redis_6379 /etc/init.d/redis
$ sudo vim /etc/init.d/redis

将文件内的redis_6379替换为redis, vim按ESC切换到命令模式, 替换后保存退出vim

:%s/redis_6379/redis/g
:wq

编辑配置文件/etc/redis/redis.conf:

$ sudo vim /etc/redis/redis.conf
. . . 
daemonize yes
. . . 
supervised systemd
. . . 
pidfile /var/run/redis.pid
. . . 
logfile /var/log/redis.log
. . . 
dir /var/lib/redis

将文件内的redis_6379替换为redis, vim按ESC切换到命令模式, 替换后保存退出vim

:%s/redis_6379/redis/g
:wq

1.5. redis服务管理

$ sudo systemctl daemon-reload
$ sudo systemctl enable redis
$ sudo systemctl status redis
● redis.service - LSB: start and stop redis
   Loaded: loaded (/etc/rc.d/init.d/redis; bad; vendor preset: disabled)
   Active: inactive (dead) since 五 2018-06-22 14:48:36 CST; 7s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 21946 ExecStop=/etc/rc.d/init.d/redis stop (code=exited, status=0/SUCCESS)
  Process: 21909 ExecStart=/etc/rc.d/init.d/redis start (code=exited, status=0/SUCCESS)
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz systemd[1]: Starting LSB: start and ...
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz redis[21909]: Starting Redis server...
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz systemd[1]: Started LSB: start and s...
6月 22 14:48:36 izm5eat3t2va6ch6mdbpbtz systemd[1]: Stopping LSB: start and ...
6月 22 14:48:36 izm5eat3t2va6ch6mdbpbtz redis[21946]: /var/run/redis.pid doe...
6月 22 14:48:36 izm5eat3t2va6ch6mdbpbtz systemd[1]: Stopped LSB: start and s...
Hint: Some lines were ellipsized, use -l to show in full.
$ sudo systemctl start redis
● redis.service - LSB: start and stop redis
   Loaded: loaded (/etc/rc.d/init.d/redis; bad; vendor preset: disabled)
   Active: active (exited) since 五 2018-06-22 14:24:50 CST; 5s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 21909 ExecStart=/etc/rc.d/init.d/redis start (code=exited, status=0/SUCCESS)
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz systemd[1]: Starting LSB: start and ...
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz redis[21909]: Starting Redis server...
6月 22 14:24:50 izm5eat3t2va6ch6mdbpbtz systemd[1]: Started LSB: start and s...
Hint: Some lines were ellipsized, use -l to show in full.

开启服务: sudo systemctl start redis
停止服务: sudo systemctl stop redis
重启服务: sudo systemctl restart redis
查看进程: sudo lsof -i:6379
杀掉进程: sudo kill -9 pid
进入redis shell: redis-cli

2. ubuntu下安装redis

以ubuntu16.04为例.

源码安装步骤与centos的基本相同。

Redis 备份、容灾及高可用实战

Redis已经大量应用于各种互联网架构场景中,其优异的性能,良好的操作性,以及大量的场景应用案例,使得Redis备受瞩目。本文作者向大家介绍了一种Redis在非大集群分布式应用场景下的灾备解决方案。一起来品读一下吧~

一,Redis简单介绍

Redis是一个高性能的key-value非关系型数据库,由于其具有高性能的特性,支持高可用、持久化、多种数据结构、集群等,使其脱颖而出,成为常用的非关系型数据库。
此外,Redis的使用场景也比较多。
会话缓存(Session Cache)
Redis缓存会话有非常好的优势,因为Redis提供持久化,在需要长时间保持会话的应用场景中,如购物车场景这样的场景中能提供很好的长会话支持,能给用户提供很好的购物体验。
全页缓存
在WordPress中,Pantheon提供了一个不错的插件wp-redis,这个插件能以最快的速度加载你曾经浏览过的页面。
队列
Reids提供list和set操作,这使得Redis能作为一个很好的消息队列平台来使用。
我们常通过Reids的队列功能做购买限制。比如到节假日或者推广期间,进行一些活动,对用户购买行为进行限制,限制今天只能购买几次商品或者一段时间内只能购买一次。也比较适合适用。
排名
Redis在内存中对数字进行递增或递减的操作实现得非常好。所以我们在很多排名的场景中会应用Redis来进行,比如小说网站对小说进行排名,根据排名,将排名靠前的小说推荐给用户。
发布/订阅
Redis提供发布和订阅功能,发布和订阅的场景很多,比如我们可以基于发布和订阅的脚本触发器,实现用Redis的发布和订阅功能建立起来的聊天系统。
此外还有很多其它场景,Redis都表现的不错。

二,Redis使用中单点故障问题

正是由于Redis具备多种优良特新,且应用场景非常丰富,以至于Redis在各个公司都有它存在的身影。那么随之而来的问题和风险也就来了。Redis虽然应用场景丰富,但部分公司在实践Redis应用的时候还是相对保守使用单节点部署,那为日后的维护带来了安全风险。
在2015年的时候,曾处理过一个因为单点故障原因导致的业务中断问题。当时的Redis都未采用分布式部署,采用单实例部署,并未考虑容灾方面的问题。
当时我们通过Redis服务器做用户购买优惠商品的行为控制,但后来由于未知原因Redis节点的服务器宕机了,导致我们无法对用户购买行为进行控制,造成了用户能够在一段时间内多次购买优惠商品的行为。
这种宕机事故可以说已经对公司造成了不可挽回的损失了,安全风险问题非常严重,作为当时运维这个系统的我来说有必要对这个问题进行修复和在架构上的改进。于是我开始了解决非分布式应用下Redis单点故障方面的研究学习。

三,非分布式场景下Redis应用的备份与容灾

Redis主从复制现在应该是很普遍了。常用的主从复制架构有如下两种架构方案。
常用Redis主从复制

  • 方案一
    未分类

这是最常见的一种架构,一个Master节点,两个Slave节点。客户端写数据的时候是写Master节点,读的时候,是读取两个Slave,这样实现读的扩展,减轻了Master节点读负载。

  • 方案二
    未分类

这种架构同样是一个Master和两个Slave。不同的是Master和Slave1使用keepalived进行VIP转移。Client连接Master的时候是通过VIP进行连接的。避免了方案一IP更改的情况。
Redis主从复制优点与不足
优点
1.实现了对master数据的备份,一旦master出现故障,slave节点可以提升为新的master,顶替旧的master继续提供服务
2.实现读扩展。使用主从复制架构, 一般都是为了实现读扩展。Master主要实现写功能, Slave实现读的功能
不足
1.架构方案一
未分类
当Master出现故障时,Client就与Master端断开连接,无法实现写功能,同时Slave也无法从Master进行复制。
此时需要经过如下操作(假设提升Slave1为Master):
1)在Slave1上执slaveof no one命令提升Slave1为新的Master节点。
2)在Slave1上配置为可写,这是因为大多数情况下,都将slave配置只读。
3)告诉Client端(也就是连接Redis的程序)新的Master节点的连接地址。
4)配置Slave2从新的Master进行数据复制。
2.架构方案二
未分类
当master出现故障后,Client可以连接到Slave1上进行数据操作,但是Slave1就成了一个单点,就出现了经常要避免的单点故障(single point of failure)。
之后需要经过如下操作:
1)在Slave1上执行slaveof no one命令提升Slave1为新的Master节点
2)在Slave1上配置为可写,这是因为大多数情况下,都将Slave配置只读
3)配置Slave2从新的Master进行数据复制
可以发现,无论是哪种架构方案都需要人工干预来进行故障转移(failover)。需要人工干预就增加了运维工作量,同时也对业务造成了巨大影响。这时候可以使用Redis的高可用方案-Sentinel

四,Redis Sentinel介绍

Redis Sentinel为Redis提供了高可用方案。从实践方面来说,使用Redis Sentinel可以创建一个无需人为干预就可以预防某些故障的Redis环境。
Redis Sentinel设计为分布式的架构,运行多个Sentinel进程来共同合作的。运行多个Sentinel进程合作,当多个Sentinel同一给定的master无法再继续提供服务,就会执行故障检测,这会降低误报的可能性。

五,Redis Sentinel功能

Redis Sentinel在Redis高可用方案中主要作用有如下功能:

  • 监控
    Sentinel会不断的检查master和slave是否像预期那样正常运行

  • 通知
    通过API,Sentinel能够通知系统管理员、程序监控的Redis实例出现了故障

  • 自动故障转移
    如果master不像预想中那样正常运行,Sentinel可以启动故障转移过程,其中的一个slave会提成为master,其它slave会重新配置来使用新的master,使用Redis服务的应用程序,当连接时,也会被通知使用新的地址。

  • 配置提供者
    Sentinel可以做为客户端服务发现的认证源:客户端连接Sentinel来获取目前负责给定服务的Redis master地址。如果发生故障转移,Sentinel会报告新的地址。

六,Redis Sentinel架构

未分类

七,Redis Sentinel实现原理

Sentinel集群对自身和Redis主从复制进行监控。当发现Master节点出现故障时,会经过如下步骤:

  • 1)Sentinel之间进行选举,选举出一个leader,由选举出的leader进行failover

  • 2)Sentinel leader选取slave节点中的一个slave作为新的Master节点。对slave选举需要对slave进行选举的方法如下:
    a) 与master断开时间
    如果与master断开的时间超过down-after-milliseconds(sentinel配置) * 10秒加上从sentinel判定master不可用到sentinel开始执行故障转移之间的时间,就认为该slave不适合提升为master。
    b) slave优先级
    每个slave都有优先级,保存在redis.conf配置文件里。如果优先级相同,则继续进行。
    c) 复制偏移位置
    复制偏移纪录着从master复制数据复制到哪里,复制偏移越大表明从master接受的数据越多,如果复制偏移量也一样,继续进行选举
    d) Run ID
    选举具有最小Run ID的Slave作为新的Master
    流程图如下:
    未分类

  • 3) Sentinel leader会在上一步选举的新master上执行slaveof no one操作,将其提升为master节点

  • 4)Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave

  • 5)Sentinel leader会让原来的master降级为slave,当恢复正常工作,Sentinel leader会发送命令让其从新的master进行复制

以上failover操作均有sentinel自己独自完成,完全无需人工干预。
总结
使用sentinel实现了Redis的高可用,当master出现故障时,完全无需人工干预即可实现故障转移。避免了对业务的影响,提高了运维工作效率。
在部署sentinel的时候,建议使用奇数个sentinel节点,最少三个sentinel节点。

redis缓存同步小伎俩

redis做缓存分为被动和主动两种,今天要说的是被动+主动结合的一个小伎俩。

主动+被动结合,有2种常见做法:

  • set流派
    • 查询时,先查redis,不命中再查mysql,将结果set到redis里缓存TTL时间
    • 更新时,先更新mysql,再set到redis里缓存TTL时间
  • delete流派
    • 查询流程同上
    • 更新时,先更新mysql,再去redis里做delete删除掉缓存

set流派适合应付读热点的场景,不希望因为delete造成缓存穿透,影响到性能。

delete流派比较平庸,没有太密集的热点读压力,偏向于海量冷数据的被动缓存,删除的性价比更高。

小伎俩

今天的小伎俩与set流派有关,主要指出一个并发竞争的case。

考虑如下的时序:

  • 查询:redis miss,于是查询了mysql得到数据A。
  • 更新:mysql数据修改为B,同时set到Redis中。
  • 查询:通过set 命令将A更新到redis中。

结局:缓存里cache了一份老数据A。

对于缓存一致性要求高的业务是无法接受这种竞争的,即便没有很高的缓存一致性要求,一般业务也不希望这种事情出现。

解决方法,查询时使用setnx命令(set if not exist)取代set,同理可以用hsetnx取代hset,不同数据结构均有对应方法。

CentOS下安装Redis并设置密码外网访问

在windows下,下载redis直接运行redis-server.exe即可,方便快捷。

未分类
redis windows

Centos下安装redis稍微复杂一点。

redis的官网 https://redis.io/

先获取到redis

http://download.redis.io/releases/redis-4.0.9.tar.gz

然后解压编译安装

tar xzf redis-4.0.9.tar.gz
cd redis-4.0.9
make
make install

未分类

编译安装完成后启动Redis

redis-server

也可以通过初始化脚本启动Redis,在编译后的目录utils文件夹中有

redis_init_script

首先将初始化脚本复制到/etc/init.d 目录中,文件名为 redis_端口号(这个mv成了redis_6379),其中端口号表示要让Redis监听的端口号,客户端通过该端口连接Redis。然后修改脚本中的 REDISPORT 变量的值为同样的端口号。

未分类

然后建立存放Redis的配置文件目录和存放Redis持久化的文件目录

/etc/redis 存放Redis的配置文件

/var/redis/端口号 存放Redis的持久化文件(这里是 /var/redis/6379 )

修改配置文件

将配置文件模板 redis-4.0.9/redis.conf 复制到 /etc/redis 目录中,以端口号命名(如 6379.conf ),然后对其中的部分参数进行编辑。

daemonize yes 使Redis以守护进程模式运行
pidfile /var/run/redis_端口号.pid 设置Redis的PID文件位置
port 端口号 设置Redis监听的端口号
dir /var/redis/端口号 设置持久化文件存放位置
#requirepass foobared 若需要设置密码就把注释打开,改成你要设置的密码
bind 127.0.0.1   将其默认的127.0.0.1改为0.0.0.0(代表不做限制),这样外网就能访问了

现在也可以使用下面的命令来启动和关闭Redis了

/etc/init.d/redis_6379 start

/etc/init.d/redis_6379 stop

redis随系统自动启动

chkconfig redis_6379 on

通过上面的操作后,以后也可以直接用下面的命令对Redis进行启动和关闭了,如下

service redis_6379 start

service redis_6379 stop

这样系统重启,Redis也会随着系统启动自动启动起来。

那么怎么停止Redis呢?

上面的stop方法可以停止redis,但是考虑到 Redis 有可能正在将内存中的数据同步到硬盘中,强行终止 Redis 进程可能会导致数据丢失。正确停止Redis的方式应该是向Redis发送SHUTDOWN命令,方法为:

redis-cli SHUTDOWN

当Redis收到SHUTDOWN命令后,会先断开所有客户端连接,然后根据配置执行持久化,最后完成退出。
Redis可以妥善处理 SIGTERM信号,所以使用 kill Redis 进程的 PID也可以正常结束Redis,效果与发送SHUTDOWN命令一样。

如果需要外网访问,首先检查是否被防火墙挡住

然后在配置文件中将bind配置项默认的127.0.0.1改为0.0.0.0

使用腾讯云可能需要在腾讯云控制台对端口进一步设置。

redis刷磁盘可能会导致瞬时无法连接

业务日志监控中报告, 每天会有大约250次连接redis失败.
通过strace追踪发现.故障的时间点时写磁盘时间超过了10s.一般在10-15s之间. redis第二次重试使用的是10s.

这个实例所有的操作都是INCR, fdatasync 会block写.

strace -Ttt -f -p 11302 -T -e  trace=fdatasync

11309 10:21:31.153900 fdatasync(116)    = 0 <0.034295>

11309 10:21:32.078747 fdatasync(116)    = 0 <7.592478>

11309 10:21:39.774959 fdatasync(116)    = 0 <10.098802>

11309 10:21:49.990623 fdatasync(116)    = 0 <2.026147>

11309 10:21:52.129676 fdatasync(116)    = 0 <0.002802>

治标:

超时时间改为15s.

治本:

正在用watchdog抓一下超过5s的堆栈.

堆栈:

[11302 | signal handler] (1499754857)

— WATCHDOG TIMER EXPIRED —

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(logStackTrace+0x3e)[0x445ace]

/lib64/libpthread.so.0(write+0x2d)[0x7f19ef3b06fd]

/lib64/libpthread.so.0(+0xf710)[0x7f19ef3b1710]

/lib64/libpthread.so.0(write+0x2d)[0x7f19ef3b06fd]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(flushAppendOnlyFile+0x4e)[0x44116e]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(serverCron+0x3b7)[0x41bb17]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(aeProcessEvents+0x1e9)[0x416b69]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(aeMain+0x2b)[0x416deb]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699(main+0x31d)[0x41e49d]

/lib64/libc.so.6(__libc_start_main+0xfd)[0x7f19ef02cd5d]

/usr/local/bin/redis-server-2.8 10.160.86.216:6699[0x415bd9]

[11302 | signal handler] (1499754857) ——–
fdatasync会在某个时间点超过10s.

看来因为写磁盘堵塞了, 把机械硬盘换成了SSD, 解决了。

redis分布式锁实践

分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的

我们先来看一个无锁的情况下会导致什么问题:

这是一个普通的更新用户年龄的功能,各层代码如下,访问controller层,一个更新,一个查询

未分类

这是service层,我们使用contdownlatch发令枪来模拟线程同时并发的情况,发令枪设为32,即32个线程同时去请求修改年龄,

未分类

这里使用线程池来提交多线程任务,看代码知道,这里我们已经有了判断年龄的操作,当查询用户查询大于0时,才去调更新用户年龄-1的方法,等下看看有没有用

未分类

这里是sql,可以看到两个sql,一个查询用户年龄,一个会执行用户年龄每次减1 ,

未分类

这里是用户数据,我们可以看到,用户UID为UR12324的用户,他的年龄是30,接着我们来调32个线程来操作减他年龄

未分类

我们请求下这个方法

未分类

然后看看结果:

未分类

未分类

可以看到库中年龄已被减为-2,在未加锁的情况下,查询较验并没有什么作用,此时如果加个synchronized或lock锁肯定能避免这种情况,但我们本文讨论的是多实例或分布式环境中,此加锁方式仍然会产生问题,感兴趣的可以试下是不是

下面我们开始实现一个redis分布式锁,来避免这种情况发生,先说说实现思路:

1、线程请求访问前先调用加锁的方法,加锁就去里生成一个随机数同时保存在线程本地变量和redis的某key中,此key设有效期为200ms,具体值根据业务执行时间自行调整,加锁成功;

2、其它线程试着访问拿出它本地变量与redis中某key进行比较,如果不一致,则说明有锁,此线程休眠一段时间,再试着加锁;

3、加锁成功的线程在操作结束后删掉它持有锁(用lua实现,保证原子性,在它比对和删除锁的过程中,其它线程不会加锁成功),让其它线程再次加锁以执行任务;

说明:锁的时间为200ms可预防线程挂掉之后死锁,200ms后会自动释放

下面看看我们写的锁代码:

片段1:使用redislock 实现lock来复写它的方法

未分类

片段2:试着加锁的方法

未分类

片段3:解锁方法,此处首先从线程本地变量获取它的随机数,然后调用lua脚本,与redis中key相比较,如果相同则删除,否则返回0;

未分类

此为lua脚本方法,用此方法可以保证判断和删除的原子性,在此过程中没有线程可以操作此key

未分类

到此为止,我们锁基本写完,来测试下有没有用:

未分类

我们在此方法前后分别加入加锁和解锁方法,使用方式和lock锁一样, 我们重新把年龄恢复到30后来测试一下吧

先看看日志

未分类

这里可以看到各个线程争夺锁的情况,再看看执行结果

未分类

未分类

这里我们可以看到虽然是32个线程并发执行,但此值并不会变为负数,加锁成功.

我们可以看到最后2个线程并没有执行方法

未分类

未分类

在具体生产环境中,比如典型的用户余额扣减,我们可以用用户UID作KEY,这样就不会造成100个用户,500个线程争夺一个锁的情况发生,100个用户会有100个锁,此时假如每个用户5个请求,一个锁只处理5个线程

大大提高锁的效率.

此时说明加锁成功,大家可以在分布式环境中测试更明显,有关极端情况下解锁失败后应该做什么也可以由我们自己决定,比redission要灵活,带锁的redis最好是单实例,在集群中可能会出问题,有机会我们再用zk实现下.

redis数据迁移方案

一、主从复制

二、数据文件拷贝

1、copy dump.rdb 到目标主机dump.rdb存储目录下后启动redis服务

$ scp /var/lib/redis/dump.rdb user@ip:/var/lib/redis/dump.rdb
# 登录目标主机后重启redis服务
$ docker restart redis

2、启动aof模式,copy appendonly.aof 到目标主机 redis 数据存储目录下后

$ redis-cli config set appendonly yes
$ scp /var/lib/redis/appendonly.aof user@ip:/var/lib/redis/appendonly.aof
$ redis-cli config set appendonly no
# 登录目标主机后加载数据
$ docker restart redis

设置Redis最大占用内存

设置Redis最大占用内存

Redis需要设置最大占用内存吗?如果Redis内存使用超出了设置的最大值会怎样?

设置Redis最大占用内存

Redis设置最大占用内存,打开redis配置文件,找到如下段落,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。修改如下所示:

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory 268435456

本机服务器redis配置文件路径:/etc/redis/6379.conf,由于本机自带内存只有1G,一般推荐Redis设置内存为最大物理内存的四分之三,所以设置0.75G,换成byte是751619276.

可以在CentOS下输入命令:find / -name redis查找redis目录:

[root@iZ94r80gdghZ ~]# find / -name redis
/usr/share/nginx/html/blog.tanteng.me/wp-content/cache/supercache/blog.tanteng.me/tag/redis
/etc/redis
/var/redis

Redis配置文件一般在etc下的redis安装目录下。

Redis使用超过设置的最大值

如果Redis的使用超过了设置的最大值会怎样?我们来改一改上面的配置,故意把最大值设为1个byte试试。

# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory 1

打开debug模式下的页面,提示错误:OOM command not allowed when used memory > ‘maxmemory’.

设置了maxmemory的选项,redis内存使用达到上限。可以通过设置LRU算法来删除部分key,释放空间。默认是按照过期时间的,如果set时候没有加上过期时间就会导致数据写满maxmemory。

如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。

LRU是Least Recently Used 近期最少使用算法。

  • volatile-lru -> 根据LRU算法生成的过期时间来删除。
  • allkeys-lru -> 根据LRU算法删除任何key。
  • volatile-random -> 根据过期设置来随机删除key。
  • allkeys->random -> 无差别随机删。
  • volatile-ttl -> 根据最近过期时间来删除(辅以TTL)
  • noeviction -> 谁也不删,直接在写操作时返回错误。

如果设置了maxmemory,一般都要设置过期策略。打开Redis的配置文件有如下描述,Redis有六种过期策略:

# volatile-lru -> remove the key with an expire set using an LRU algorithm
# allkeys-lru -> remove any key accordingly to the LRU algorithm
# volatile-random -> remove a random key with an expire set
# allkeys-random -> remove a random key, any key
# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# noeviction -> don't expire at all, just return an error on write operations

那么打开配置文件,添加如下一行,使用volatile-lru的过期策略:

maxmemory-policy volatile-lru

保存文件退出,重启redis服务。

info命令查看Redis内存使用情况

如服务器Redis所在目录:/usr/local/redis-3.0.7/src

在终端输入./redis-cli,打开Redis客户端,输入info命令。

出来如下信息:

[root@iZ94r80gdghZ src]# ./redis-cli
127.0.0.1:6379> info
# Server
redis_version:3.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:f07a42660a61a05e
redis_mode:standalone
os:Linux 3.10.0-327.10.1.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.5
process_id:2165
run_id:8ec8a8dc969d6e2f2867d9188ccb90850bfc9acb
tcp_port:6379
uptime_in_seconds:668
uptime_in_days:0
hz:10
lru_clock:15882419
config_file:/etc/redis/6379.conf

# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

# Memory
used_memory:816232
used_memory_human:797.10K
used_memory_rss:7655424
used_memory_peak:816232
used_memory_peak_human:797.10K
used_memory_lua:36864
mem_fragmentation_ratio:9.38
mem_allocator:jemalloc-3.6.0

# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1458722327
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok

# Stats
total_connections_received:1
total_commands_processed:0
instantaneous_ops_per_sec:0
total_net_input_bytes:14
total_net_output_bytes:0
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
migrate_cached_sockets:0

# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

# CPU
used_cpu_sys:0.30
used_cpu_user:0.29
used_cpu_sys_children:0.00
used_cpu_user_children:0.00

# Cluster
cluster_enabled:0

# Keyspace
db0:keys=1,expires=1,avg_ttl=425280

其中used_memory:816232,仅用了0.7M左右。

本文在网上查阅资料,在阿里云CentOS主机实践。

使用 Docker Compose 本地部署基于 Sentinel 的高可用 Redis 集群

说明

项目地址:github.com/TomCzHen/re…

根据官方文档 Redis Sentinel Documentation 中的 Example 2: basic setup with three boxes 示例创建的实例,但因为是单机部署,所以不满足 Redis 实例 与 Sentinel 实例分别处于 3 台机器的要求,因此仅用于开发环境测试与学习。

使用方法

  • 使用 docker-compose up -d 部署运行。

  • 使用 docker-compose pause master 可以模拟对应的 Redis 实例不可用。

  • 使用 docker-compose pause sentinel-1 可以模拟对应的 Sentinel 实例不可用。

  • 使用 docker-compose unpause service_name 将暂停的容器恢复运行。

  • 使用支持 Sentinel 的客户端连接 localhost:62379 进行应用测试。

注:Windows 和 Mac 可能需要修改 Volumes 挂载参数。

注意事项

Sentinel, Docker, NAT, and possible issues

https://redis.io/topics/sentinel#sentinel-docker-nat-and-possible-issues

将容器端口 EXPOSE 时,Sentinel 所发现的 master/slave 连接信息(IP 和 端口)对客户端来说不一定可用。

例如:将 Reids 实例端口 6379 EXPOSE 为 16379, Sentinel 容器使用 LINK 的方式访问 Redis 容器,那么对于 Sentinel 容器 6379 端口是可用的,但对于外部客户端是不可用的。

解决方法是 EXPOSE 端口时保持内外端口一致,或者使用 host 网络运行容器。如果你想使用本项目中的编排文件部署的集群对外部可用,那么只能将 Redis 容器运行在 host 网络之上。

注:实际上 bridge 模式下 Redis 性能也会受到影响。

文件结构

.
├── docker-compose.yaml
├── nginx
│   └── nginx.conf
├── README.md
├── .env
└── sentinel
    ├── docker-entrypoint.sh
    ├── Dockerfile-sentinel
    └── sentinel.conf.sample

Sentinel

镜像使用 Dockerfile-sentinel 构建,运行时根据环境变量生成 sentinel.conf 文件,详细配置说明请查看 sentinel.conf.sample 内容。

docker-entrypoint.sh

使用 Reids 官方镜像中的 docker-entrypoint.sh 脚本修改而来,添加了生成 sentienl.conf 语句。

...
# create sentinel.conf
if [ ! -e ${SENTINEL_CONF_PATH} ]; then
    envsubst < /etc/redis/sentinel.conf.sample > ${SENTINEL_CONF_PATH}
    chown redis:redis /etc/redis/sentinel.conf
fi
...

修改配置 Sentinel 的环境变量后需要重新创建容器才能生效。

可用环境变量

SENTINEL_CONF_PATH=/etc/redis/sentinel.conf
SENTINEL_PORT=26379
SENTINEL_MASTER_NAME=redis-master
SENTINEL_REDIS_IP=127.0.0.1
SENTINEL_REDIS_PORT=6379
SENTINEL_REDIS_PWD=
SENTINEL_QUORUM=2
SENTINEL_DOWN_AFTER=30000
SENTINEL_PARALLEL_SYNCS=1
SENTINEL_FAILOVER_TIMEOUT=180000

docker-compose.yaml

可以使用 docker-compose config 可查看完整的编排内容。

Redis 实例运行参数

详细可用参数请查看官方示例文件 Redis Configuration File Example,需要注意 port 参数需要与编排中的 PORTS 保持一致,或修改编排文件让容器网络使用 host 模式。

由于 master 会被 Sentinel 切换为 slave ,因此最好保持每个 Redis 实例的口令一致。

master:
    image: redis:4.0.8-alpine
    ports:
      - 6379:6379
    volumes:
      - type: volume
        source: master-data
        target: /data
    command: [
      '--requirepass "${REDIS_PWD}"',
      '--masterauth "${REDIS_PWD}"',
      '--maxmemory 512mb',
      '--maxmemory-policy volatile-ttl',
      '--save ""',
    ]

Sentinel 实例运行参数

详细可用参数请查看 sentinel 目录下的 sentinel.conf.sample 文件。由于容器使用的配置文件是运行时根据环境变量生成的,因此使用 environment 进行配置,可用环境变量请查看文档 Sentinel 部分。

最后使用了 Nginx 作为 Sentinel 实例的代理,因此 Sentinel 容器不需要对外访问。

sentinel-1: &sentinel
    build:
      context: ./sentinel
      dockerfile: Dockerfile-sentinel
    image: redis-sentinel:dev
    environment:
      - SENTINEL_REDIS_PWD=${REDIS_PWD}
      - SENTINEL_REDIS_IP=${SENTINEL_MASTER_NAME}
      - SENTINEL_QUORUM=2
      - SENTINEL_DOWN_AFTER=3000
    command: [
      '${SENTINEL_CONF_PATH}',
      '--sentinel'
    ]
    depends_on:
      - master
      - node-1
      - node-2
    links:
      - master:${SENTINEL_MASTER_NAME}
      - node-1
      - node-2
  sentinel-2:
    <<: *sentinel
  sentinel-3:
    <<: *sentinel

Nginx

使用 Nginx 作为 Sentinel 负载均衡以及高可用代理。

  nginx:
    image: nginx:1.13.9-alpine
    ports:
      - 26379:26379
    volumes:
      - type: bind
        source: ./nginx/nginx.conf
        target: /etc/nginx/nginx.conf
        read_only: true
    depends_on:
      - sentinel-1
      - sentinel-2
      - sentinel-3

修改 nginx 目录下的 nginx.conf 进行配置。

...
stream {
    server {
        listen 26379;
        proxy_pass redis_sentinel;
    }

    upstream redis_sentinel {
        server sentinel-1:26379;
        server sentinel-2:26379;
        server sentinel-3:26379;
    }
}
...