0%

Redis各种实战

黑名单一词来源于世界著名的英国的牛津和剑桥等大学。在中世纪初这些学校规定对于犯有不端行为的学生,将其姓名、行为列案记录在黑皮书上,谁的名字上了黑皮书,即使不是终生臭名昭著,也会使人在相当时间内名誉扫地。学生们对学校的这一规定十分害怕,常常小心谨慎,严防越轨行为的发生。在网络SEO优化当中,搜索引擎或者义务用户收集的搜索引擎垃圾制造者列表,可以用于从搜索引擎封杀这些垃圾制造者,或者抵制他们。黑名单启用后,被列入到黑名单的用户(或IP地址、IP包、邮件、病毒等)不能通过。白名单的概念与“黑名单”相对应,如果设立了白名单,则在白名单中的用户(或IP地址、IP包、邮件等)会优先通过,不会被当成垃圾邮件拒收,安全性和快捷性都大大提高。将其含义扩展一步,那么凡有黑名单功能的应用,就会有白名单功能与其对应。

访问白名单

一、准备工作

  1. 添加echo-nginx-module模块
    • git clone https://github.com/openresty/echo-nginx-module.git /usr/local/echo-nginx-module
    • 切换到nginx源码目录 cd /usr/local/src/nginx
    • 重新执行 ./configure --prefix=/usr/local/nginx --add-module=/usr/local/echo-nginx-module
    • 若提示错误少什么依赖就安装相应的依赖,无提示则执行 make && make install

网上还有另一种方法,同理可以添加别的模块

1
2
3
4
git clone https://github.com/nginx/nginx.git
git submodule add git@github.com:openresty/echo-nginx-module.git
#重新配置nginx,把echo-nginx-module模块编译进nginx可执行文件
sudo ./configure --add-module=echo-nginx-module
  1. 安装ngx_devel_kit(NDK)模块

    • cd /usr/local
    • git clone https://github.com/simpl/ngx_devel_kit.git
  2. 添加lua-nginx-module模块

    • cd /usr/local
    • git clone https://github.com/chaoslawful/lua-nginx-module.git
  3. 重新编译nginx

    1
    2
    3
    4
    5
    6
    7
    ./configure --prefix=/usr/local/nginx 
    --with-ld-opt="-Wl,-rpath,$LUAJIT_LIB"
    --add-module=/usr/local/ngx_devel_kit
    --add-module=/usr/local/echo-nginx-module
    --add-module=/usr/local/lua-nginx-module
    make -j2
    make install
  4. 重启nginx服务器

  5. 测试Lua,在nginx.conf配置文件server代码块中加入以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    location /echo { 
    default_type 'text/plain';
    echo 'install echo module success!';
    }
    location /lua {
    default_type 'text/plain';
    content_by_lua 'ngx.say("install lua module success!")';
    }
  6. curl http://localhost/echo

    • 正常的话输出install echo module success!
  7. curl http://localhost/lua

    • 正常的话输出install lua module success!

二、实现

  1. 实现原理:通过在nginx上进行访问限制,通过lua来灵活实现业务需求,redis用于存储黑名单列表

  2. 具体过程

    • step1:lua代码(post请求,ip地址黑名单,请求参数中imsi,tel值和黑名单)
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
[root@git-server ~]# vim /usr/local/nginx/conf/lua/ipblacklist.lua

ngx.req.read_body()

local redis = require "resty.redis"
local red = redis.new()
red.connect(red, '127.0.0.1', '6379')

local myIP = ngx.req.get_headers()["X-Real-IP"]
if myIP == nil then
myIP = ngx.req.get_headers()["x_forwarded_for"]
end
if myIP == nil then
myIP = ngx.var.remote_addr
end

if ngx.re.match(ngx.var.uri,"^(/webapi/).*$") then
local method = ngx.var.request_method
if method == 'POST' then
local args = ngx.req.get_post_args()
local hasIP = red:sismember('black.ip',myIP)
local hasIMSI = red:sismember('black.imsi',args.imsi)
local hasTEL = red:sismember('black.tel',args.tel)
if hasIP==1 or hasIMSI==1 or hasTEL==1 then
--ngx.say("This is 'Black List' request")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
else
--ngx.say("This is 'POST' request")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
end
  • step2:修改nginx.conf

    1
    2
    3
    4
    5
    6
    7
    location / {
    root html;
    index index.html index.htm;
    access_by_lua_file /usr/local/nginx/conf/lua/ipblacklist.lua;
    proxy_pass http://127.0.0.1:8080;
    client_max_body_size 1m;
    }
  • step3:添加黑名单规则数据

    1
    2
    3
    redis-cli sadd black.ip '192.160.10.10'
    redis-cli sadd black.imsi '460123456789'
    redis-cli sadd black.tel '15888888888'
  • step4:验证结果curl -d "imsi=460123456789&tel=15800000000" "http://yourdomain/index.php"

三、参考


缓存和数据库数据一致性问题

一、渊源

      随着计算机技术的飞速发展,web应用由最初的单一节点到多节点负载均衡再到如今的分布式,每一次发展都会伴随着技术上的革新,同时也会带来新的问题。如现在随随便便一个应用设计都需要考虑的“三高一低”的问题,而要满足这样的要求不可避免的会用到缓存、消息队列等技术,这其中就涉及到缓存和数据库数据一致性的问题。

二、解决方案

  1. 方案一:缓存设置过期时间,最终一致:对写入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。如果数据库写成功,缓存更新失败,那么只要到达过期时间缓存会自动删除,后面的读请求自然会从数据库中读取新值然后回填缓存。

    • 存在问题
      • 短时间内为旧数据
      • 过期时间如何设置(设置不当会出现雪崩)
    • 适用场景
      • 对数据实时性要求不高
  2. 方案二:先更新数据库,再更新缓存(很少使用)

    • 存在问题

      • 资源浪费(频繁更新冷数据,或写多读少的情景,缓存命中率低)
      • 存在脏数据,由于网络原因,可能会出现以下情形,从而导致A数据覆盖了B数据的情况:
      1
      2
      3
      4
      请求A更新了数据库
      请求B更新了数据库
      请求B更新了缓存
      请求A更新了缓存
      • 请求响应时间延长(每次更新缓存值都需要经过复杂的计算)
    • 适用场景

      • 读请求占据网站的大部分流量
      • 网站数据量不大(几十万的文章数据)
      • 很少会去更新数据(一般文章写好后,不会去更新)
  3. 方案三:先更新数据库,再删除缓存(Cache Aside Pattern)

    • 存在问题

      • 存在脏数据,由于网络原因,可能会出现以下情形,从而导致A数据覆盖了B数据的情况:
      1
      2
      3
      4
      5
      6
      用户1请求数据A
      数据A缓存失效
      用户1从数据库中得到旧数据数据A
      用户2更新了数据A(新数据)
      用户2删除了缓存
      用户1将查到旧数据写入了缓存
      • 缓存删除失败,由于某种原因导致缓存删除失败
      1
      2
      3
      用户A更新了数据A
      用户A删除数据A的缓存失败
      用户B读到数据A缓存的旧数据
    • 解决问题

      • 设置缓存有效时间
      • 引入消息队列
        • 更新数据库
        • 删除缓存失败
        • 将需要删除的Key发送到消息队列
        • 隔断时间从消息队列中拉取要删除的key
        • 继续删除,直至成功为止
  4. 方案四:先删除缓存,再更新数据库

    • 存在问题

      • 存在脏数据
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      用户A删除缓存失败
      用户A成功更新了数据
        
      或者

      用户A删除了缓存;
      用户B读取缓存,缓存不存在;
      用户B从数据库拿到旧数据;
      用户B更新了缓存;
      用户A更新了数据。
    • 解决问题

      • 设置缓存有效时间
      • 引入消息队列
      1
      2
      3
      4
      5
      先淘汰缓存;
      更新数据库;
      将需要淘汰的缓存Key发送到消息队列;
      另起一程序拉取消息队列的数据;
      对需要删除的key进行删除,直至删除为止。
  5. 方案五:引入重试机制

    • 更新数据库数据;
    • 缓存因为种种问题删除失败
    • 将需要删除的key发送至消息队列
    • 自己消费消息,获得需要删除的key
    • 继续重试删除操作,直到成功
  6. 方案六:重试+binlog

    • 更新数据库数据
    • 数据库会将操作信息写入binlog日志当中
    • 订阅程序提取出所需要的数据以及key
    • 另起一段非业务代码,获得该信息
    • 尝试删除缓存操作,发现删除失败
    • 将这些信息发送至消息队列
    • 重新从消息队列中获得该数据,重试操作。

三、参考文献

  1. 参考一
  2. 参考二
  3. 参考三

秒杀系统

一、概念

  1. 秒杀系统:同一时刻有大量请求争抢购买同一商品,如抢红包、热卖商品、12306购票等。
  2. 两个问题
    • 高并发对数据库产生的压力
    • 竞争状态下如何解决库存的正确减少(”超卖”问题)
  3. 常见解决方案
    • 方案一:将库存字段number字段设为unsigned,当库存为0时返回false
    • 方案二:使用MySQL的事务,锁住操作的行(select … for update)
    • 方案三:使用文件排他锁
    • 方案四:使用redis队列(pop操作是原子的)

二、优化

  1. 优化方向

    • 将请求拦截在系统上游
    • 充分利用缓存
    • 异步处理请求
  2. 优化细节

    • 浏览器层请求拦截
      • 产品层面,用户点击“查询”或者“购票”后按钮置灰,禁止用户重复提交请求
      • 页面层面,限制用户在x秒之内只能提交一次请求(JS控制)
    • 站点层请求拦截与页面缓存
      • 同一个uid限制访问频度,做页面缓存,x秒内到达站点层的请求均返回同一页面
      • 同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求均返回同一页面
    • 数据缓存
      • 读请求读缓存(redis、memcache)
      • 过滤相同请求(如redis的set数据类型存储请求ID,做判重处理)
    • 异步处理
      • 写请求入队列依次处理
    • db层批量处理数据

三、方案

  1. 服务单一职责,微服务设计思想,再用分布式的部署方式

  2. 给秒单独的服务,单独的库,就算挂了, 也不会影响其他服务

  3. 秒杀连接加盐,把url动态化,就连写代码的人都不知道,通过md5之类的加密算法随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。

  4. 使用redis集群

  5. 使用nginx服务器:高性能的web服务器,并发也是随便几万不是梦,但是tomcat只能顶几百的并发啊。那简单啊负载均衡嘛,一台服务器几百,那就多搞点,在秒杀的时候多租点流量机

    • 恶意请求拦截也需要用它,一般单个用户请求次数太夸张,不像真人的请求在网关那一层就得拦截掉了
  6. 资源静态化:秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所有页面一般是不会经过后端的,但是前段也要有自己的服务器啊,那就把能提前放到cdn服务器的东西都放进去,反正把能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力

  7. 按钮控制:秒杀前按钮置灰,到点了才能点。这是防止在快到秒杀前的时间疯狂请求服务器,这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点了再给按钮可以用,点击一次之后也得置灰几秒,防止一直点。

  8. 限流

    • 前端限流:跟按钮控制类似,防止一直点
    • 后端限流:秒杀的时候肯定是涉及到了后续的订单生成和支付操作,一旦秒杀产品卖完了,return一个false,前端直接秒杀结束
    • 真正的限流还会有限流组件,比如阿里的Sentinel、Hystrix等
  9. 库存预热:秒杀的本质,就是对库存的争夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,对开发很不友好,而且数据库顶不住啊
    提前把商品的库存加载到redis中去,让整个流程都在redis里做,然后等秒杀结束了,再异步的去修改库存就好了

  10. Lua:lua脚本类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作,一个脚本=查库存+扣减库存,如果是0 了直接false

  11. 削峰填谷:说到这里就知道是说mq了,卖100个东西直接100个请求,我觉得没问题,但万一秒杀一万个,10万个呢,服务器挂了,把他放消息队列,然后一点点消费去改库存不就好了嘛

  12. 异步:一个下单流程:本来需要100ms,后来产品说要加上积分,流程中加上积分扣减,200ms了

秒杀系统怎么才能抢购两次

四、简单DEMO

  1. 初始化库存
1
2
3
4
5
6
7
8
$store  = 1000;  //商品库存
$redis = new Redis();
$result = $redis->connect('127.0.0.1',6379);
$res = $redis->llen('goods_store');
for($i = 0; $i < $store; $i++) {
$redis->lpush('goods_store',1);
}
echo $redis->llen('goods_store');
  1. 用户下单
1
2
3
4
5
6
7
$redis  = new Redis();  
$result = $redis->connect('127.0.0.1',6379);
$count = $redis->lpop('goods_store');
if(!$count) {
echo '抢购失败!';
return;
}

五、参考

  1. 参考一
  2. 参考二
  3. 参考三
  4. 参考四

消息队列的几种方式

一、概念

      队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。它是一种先进先出的数据结构,即操作队列元素时有顺序性。

      消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,以及Redis。

二、实现方式

  1. 基于List
    • 非阻塞命令LPUSH/RPOP
    • 阻塞命令BRPOP

此处的阻塞与非阻塞理解:基于List实现的消息队列,消费者端一般写个死循环(while(true))不断的从队列中拉取消息;当队列为空时,消费者端使用RPOP时仍然会不断拉取消息,进而造成CPU空跑浪费资源;BRPOP则会阻塞,当队列不为空时会重新拉取(在设置的超时时间范围内)。

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
# 客户端1
127.0.0.1:6379> lpush mq-list 1 2 3
(integer) 3
127.0.0.1:6379> type mq-list
list
127.0.0.1:6379> llen mq-list
(integer) 3

# 客户端2
127.0.0.1:6379> rpop mq-list
"1"
127.0.0.1:6379> rpop mq-list
"2"
127.0.0.1:6379> rpop mq-list
"3"
127.0.0.1:6379> rpop mq-list
(nil)


# 客户端1
127.0.0.1:6379> llen mq-list
(integer) 0

# 客户端2
127.0.0.1:6379> brpop mq-list 0

# 客户端1
127.0.0.1:6379> lpush mq-list 222
(integer) 1

# 客户端2
127.0.0.1:6379> brpop mq-list 0
1) "mq-list"
2) "222"
(17.27s)
  1. 基于Pub/Sub
1
2
3
4
5
6
7
8
9
10
11
12
13
# 客户端1
127.0.0.1:6379> subscribe mq-ps
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mq-ps"
3) (integer) 1
1) "message"
2) "mq-ps"
3) "hello world"

# 客户端2
127.0.0.1:6379> publish mq-ps "hello world"
(integer) 1
  1. 基于Zset

    • 根据score值的有序性可实现顺序消费的消息队列
  2. 基于Stream(5.0+版本支持)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 客户端1
127.0.0.1:6379> xadd mq-stream * name liusir
"1618810421154-0"
127.0.0.1:6379> xadd mq-stream * name liuliu
"1618810463678-0"

# 客户端2
127.0.0.1:6379> xread count 5 STREAMS mq-stream 0-0
1) 1) "mq-stream"
2) 1) 1) "1618810421154-0"
2) 1) "name"
2) "liusir"
2) 1) "1618810463678-0"
2) 1) "name"
2) "liuliu"

三、参考

  1. 参考一
  2. 参考二

亿级别活跃度以及登录次数统计

一、bitmaps

  1. 定义

        redis提供的bitmaps这个“数据结构”可以实现对位的操作,其本身不是一种数据类型而是字符串,但它可以对字符串的位进行操作。

  • 可以把bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量。
  • 单个bitmaps的最大长度是512MB,即2^32个比特位。
  1. 命令

    • 设置值 setbit key offset value
    • 获取值 getbit key offset
    • 统计bitmaps值为1的个数 bitcount key [start end]
    • 计算交集 bitop and destkey key[key…]
    • 计算并集 bitop or destkey key[key…]
    • 计算差集 bitop xor destkey key[key..]

    setbit/getbit命令中offsetbitcount命令中startend有个对应关系,offset*8=start/end

二、实现

三、参考


分布式锁

一、命令

  1. 旧版本

    • setnx my_key myvalue
    • expire my_key 5
    • del my_key
  2. 新版本

    • set key value EX 60 NX

二、DEMO

  1. 基于setnx版本(多个命令无法保证事务性操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$key = 'test';
$lockKey = 'lock:' . $key;
$expire = 60;
$result = $redis->get($lockKey);
if (empty($result)) {
$status = true;
while ($status) {
$lockValue = time() + $expire;
$lock = $redis->setnx($lockKey, $lockValue);
if ($lock || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() )) {
$redis->expire($lockKey, $expire);
if ($redis->ttl($lockKey) < 0) {
$redis->del($lockKey);
}
$status = false;
} else {
sleep(2);
}
}
}
  1. 基于lua版本

  2. 基于set版本

三、参考

  1. 参考一