后端杂七杂八系列篇二

本文详细介绍了Redis中的消息队列、发布-订阅模式、Lua脚本的使用(包括EVAL、SCRIPTLOAD等)、限流算法(固定窗口、滑动窗口、令牌桶和漏桶)以及SpringBoot中集成Redis的示例,展示了Lua在Redis中的实际应用和重试框架SpringRetry的使用。
摘要由CSDN通过智能技术生成

① Redis–消息队列

在这里插入图片描述

① 发布-订阅模式

在这里插入图片描述

② 发布-订阅常见的模型

① 一个发布者多个订阅者模型
在这里插入图片描述
② 多个发布者一个订阅者模型

在这里插入图片描述

③ 多个发布者多个订阅者模型
在这里插入图片描述

③ 发布-订阅redis命令


在这里插入图片描述

④ 发布-订阅模式redis实战


一个发布者多个订阅者模型

# 先订阅到一个频道
127.0.0.1:6379> SUBSCRIBE FM888
1) "subscribe"
2) "FM888"
3) (integer) 1
 
# 发布者发布消息
127.0.0.1:6379> PUBLISH FM888 test1
(integer) 1
127.0.0.1:6379> PUBLISH FM888 test2
(integer) 2 (代表订阅个数)
 
# 订阅者们接收消息
127.0.0.1:6379(subscribed mode)> SUBSCRIBE FM888
1) "subscribe"
2) "FM888"
3) (integer) 1
1) "message"
2) "FM888"
3) "test1"
1) "message"
2) "FM888"
3) "test2"

多个发布者个一个订阅者模型

# 订阅者订阅频道
127.0.0.1:6379(subscribed mode)> SUBSCRIBE FM888
 
# 发布者1
127.0.0.1:6379> PUBLISH FM888 test2
(integer) 1
 
# 发布者2
127.0.0.1:6379> PUBLISH FM888 test3
(integer) 1
 
# 订阅者
127.0.0.1:6379(subscribed mode)> SUBSCRIBE FM888
1) "subscribe"
2) "FM888"
3) (integer) 1
1) "message"
2) "FM888"
3) "test2"
1) "message"
2) "FM888"
3) "test3"

多个发布者个多个订阅者模型

# 订阅者们订阅频道
127.0.0.1:6379> PSUBSCRIBE *
 
# 发布者1
127.0.0.1:6379> PUBLISH FM3 test10
(integer) 1
 
# 发布者2
127.0.0.1:6379> PUBLISH FM888 test10
(integer) 1
 
# 发布者3
127.0.0.1:6379> PUBLISH FM888 test1
(integer) 1
 
# 发布者4
127.0.0.1:6379> PUBLISH FM12 test12
(integer) 1
 
# 订阅者们收到的消息
127.0.0.1:6379> PSUBSCRIBE *
1) "psubscribe"
2) "*"
3) (integer) 1
1) "pmessage"
2) "*"
3) "FM3"
4) "test10"
1) "pmessage"
2) "*"
3) "FM888"
4) "test10"
1) "pmessage"
2) "*"
3) "FM888"
4) "test1"
1) "pmessage"
2) "*"
3) "FM12"
4) "test12"

② Redis+Lua

① 什么是Lua?

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。Redis 2.6 版本通过内嵌支持 Lua 环境。也就是说一般的运用,是不需要单独安装Lua。


使用 Lua 脚本最大的好处是 Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,可以保持原子性且减少了网络开销。

② Lua常用的命令

常用的命令不多,就下面这几个

  1. EVAL
  2. EVALSHA
  3. SCRIPT LOAD
  4. SCRIPT EXISTS
  5. SCRIPT FLUSH
  6. SCRIPT KILL

① eval命令

eval script numkeys key [key ...] arg [arg ...]

script 参数: 是一段 Lua 脚本程序,它可以是一段程序也可以是一个文件。
numkeys参数: 指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0
key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
arg [arg …]: 附加参数,通过ARGV[1],ARGV[2]获取

// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"
// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"
// 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second 
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 
1) "key1"
2) "key2"
3) "first"
4) "second"

Lua 脚本中执行 Redis 命令

redis.call(command, key [key …] argv [argv…])

command:Redis 中的命令,如 set、get 等。
key:操作 Redis 中的 key 值,相当于我们调用方法时的形参。
param:代表参数,相当于我们调用方法时的实参。

② SCRIPT LOAD命令 和 EVALSHA命令

SCRIPT LOAD命令格式:SCRIPT LOAD script

EVALSHA命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]

SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

简单来说,就是执行SCRIPT LOAD命令会存到缓存,执行EVALSHA会校验缓存中是否有,有就执行缓存的。

## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"

## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"

② SCRIPT EXISTS 和 SCRIPT FLUSH

SCRIPT EXISTS sha1 [sha1 …]
判断脚本是否在缓存中。

SCRIPT FLUSH sha
清除Redis服务端所有 Lua 脚本缓存

③ SCRIPT KILL

SCRIPT FLUSH
杀死当前正在运行的 Lua 脚本

③ Lua 写法的Demo

Demo 1

// Redis_CompareAndSet.lua 文件
local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
        redis.call('SET', KEYS[1], ARGV[2])
        return 1
else
        return 0
end
// 执行命令
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 
## Redis客户端执行
127.0.0.1:6379> set userName zhangsan 
OK
127.0.0.1:6379> get userName
"zhangsan"

Demo 2

例如:同一IP在10秒内最多访问三次

// Redis_LimitIpVisit.lua
local visitNum = redis.call('incr', KEYS[1])

if visitNum == 1 then
        redis.call('expire', KEYS[1], ARGV[1])
end

if visitNum > tonumber(ARGV[2]) then
        return 0
end

return 1;
## LimitIP:127.0.0.1为key, 10 3表示:同一IP10秒内最多访问三次
## 前三次返回1,代表未被限制;第四、五次返回0,代表127.0.0.1这个ip已被拦截
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
 (integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
 (integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
 (integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
 (integer) 0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
 (integer) 0

Demo 3

Springboot 继承 Redis 使用 Lua

# Redis数据库地址 
spring.redis.host=127.0.0.1 
# Redis端口 
spring.redis.port=6379 
# Redis密码(如果没有密码不用填写) 
spring.redis.password= 

// RedisCRUD.lua脚本

-- set 
if KEYS[1] and ARGV[1] then 
redis.call('SET', KEYS[1], ARGV[1]) 
return 1 
end 
-- get 
if KEYS[1] and not ARGV[1] then 
return redis.call('GET', KEYS[1]) 
end 
-- delete 
if KEYS[1] and not ARGV[1] then 
redis.call('DEL', KEYS[1]) 
return 1 
end 
-- exists 
if KEYS[1] and not ARGV[1] then 
    if redis.call('EXISTS', KEYS[1]) == 1 then 
    return true 
    else 
    return false 
    end 
end 
-- hset 
if KEYS[1] and ARGV[1] and ARGV[2] and ARGV[3] then 
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2]) 
redis.call('EXPIRE', KEYS[1], ARGV[3]) 
return 1 
end 
-- hget 
if KEYS[1] and ARGV[1] and not ARGV[2] then 
return redis.call('HGET', KEYS[1], ARGV[1]) 
end 
-- hdelete 
if KEYS[1] and ARGV[1] and not ARGV[2] then 
redis.call('HDEL', KEYS[1], ARGV[1]) 
return 1 
end 
-- hexists 
if KEYS[1] and ARGV[1] and not ARGV[2] then 
    if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then 
    return true 
    else 
    return false 
    end 
end

// 在RedisTemplate的Bean中添加

 @Bean 
 public RedisScript<Long> redisScript() {
     RedisScript<Long> redisScript = new DefaultRedisScript<>(); 
     redisScript.setLocation(new ClassPathResource("lua/RedisCRUD.lua"));
     redisScript.setResultType(Long.class); 
     return redisScript; 
 } 

// 实现RedisService

@Service 
public class RedisServiceImpl implements RedisService { 
    @Autowired 
    private RedisTemplate<String, Object> redisTemplate; 
    @Autowired 
    private RedisScript<Long> redisScript;
    
    public void set(String key, Object value) { 
        redisTemplate.opsForValue().set(key, value); 
    } 
    public Object get(String key) { 
        return redisTemplate.opsForValue().get(key); 
    } 
    public void delete(String key) { 
        redisTemplate.delete(key); 
    } 
    public Boolean exists(String key) { 
        return redisTemplate.hasKey(key); 
    } 
    public Long hset(String key, String field, Object value) { 
        return redisTemplate.opsForHash().put(key, field, value); 
    } 
    public Object hget(String key, String field) { 
        return redisTemplate.opsForHash().get(key, field); 
    } 
    public void hdelete(String key, String... fields) { 
        redisTemplate.opsForHash().delete(key, fields); 
    } 
    public Boolean hexists(String key, String field) {
       return redisTemplate.opsForHash().hasKey(key, field); 
    } 
    public Long eval(String script, List<String> keys, List<Object> args) { 
        return redisTemplate.execute(RedisScript.of(script), keys, args.toArray()); 
    } 
    public Long eval(List<String> keys, List<Object> args) { 
        return redisTemplate.execute(redisScript, keys, args.toArray()); 
    } 
 } 

//测试RedisService

@RunWith(SpringRunner.class) 
@SpringBootTest 
public class RedisServiceImplTest { 
    @Autowired 
    private RedisService redisService; 
    @Test 
    public void test() {
        //第一种方式:执行string的lua
        redisService.eval("redis.call('SET', KEYS[1], ARGV[1])",Collections.singletonList(hashKey), Collections.singletonList(hashValue));
        //第二种方式:执行lua脚本
        String key ="key";
        String value ="value";
        redisService.eval(Collections.singletonList(hashKey), Collections.singletonList(hashValue));
    }

③ 常见的限流算法

在这里插入图片描述

① 固定窗口算法

又称计数器算法:在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。如图所示,我们要求3秒内的请求不要超过150次:

在这里插入图片描述

但是,貌似看似很“完美”的流量统计方式其实存在一个非常严重的临界问题,即:如果第2到3秒内产生了150次请求,而第3到4秒内产生了150次请求,那么其实在第2秒到第4秒这两秒内,就已经发生了300次请求了,远远大于我们要求的3秒内的请求不要超过150次这个限制。

在这里插入图片描述

② 滑动窗口算法

在这里插入图片描述

③ 令牌桶限流算法(控制令牌生成速度,取的速度不控制)

在这里插入图片描述

在这里插入图片描述

④ 漏桶限流算法(控制水滴流出速度,不控制水滴产生速度)

在这里插入图片描述

④ Spring Retry

重试框架,就是当我们方法失败后的重试机制

@Service
@Slf4j
public class SpringRetryAnnotationService {
    @Autowired
    CommonService commonService;

    /**
     * 如果失败,定义重试3次,重试间隔为3s,指定恢复名称,指定监听器
     */
    @Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(value = 3000L), recover = "testRecover", listeners = {"myRetryListener"})
    public void test() {
        commonService.test("注解式");
    }

    @Recover
    public void testRecover(RuntimeException runtimeException) {
        commonService.recover("注解式");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值