后端杂七杂八系列篇二

后端杂七杂八系列篇二

  • [① Redis--消息队列](#① Redis--消息队列)
    • [① 发布-订阅模式](#① 发布-订阅模式)
    • [② 发布-订阅常见的模型](#② 发布-订阅常见的模型)
    • [③ 发布-订阅redis命令](#③ 发布-订阅redis命令)
    • [④ 发布-订阅模式redis实战](#④ 发布-订阅模式redis实战)
  • [② Redis+Lua](#② Redis+Lua)
    • [① 什么是Lua?](#① 什么是Lua?)
    • [② Lua常用的命令](#② Lua常用的命令)
      • [① eval命令](#① eval命令)
      • [② SCRIPT LOAD命令 和 EVALSHA命令](#② SCRIPT LOAD命令 和 EVALSHA命令)
      • [② SCRIPT EXISTS 和 SCRIPT FLUSH](#② SCRIPT EXISTS 和 SCRIPT FLUSH)
      • [③ SCRIPT KILL](#③ SCRIPT KILL)
    • [③ Lua 写法的Demo](#③ Lua 写法的Demo)
      • [Demo 1](#Demo 1)
      • [Demo 2](#Demo 2)
      • [Demo 3](#Demo 3)
  • [③ 常见的限流算法](#③ 常见的限流算法)
    • [① 固定窗口算法](#① 固定窗口算法)
    • [② 滑动窗口算法](#② 滑动窗口算法)
    • [③ 令牌桶限流算法(控制令牌生成速度,取的速度不控制)](#③ 令牌桶限流算法(控制令牌生成速度,取的速度不控制))
    • [④ 漏桶限流算法(控制水滴流出速度,不控制水滴产生速度)](#④ 漏桶限流算法(控制水滴流出速度,不控制水滴产生速度))
  • [④ Spring Retry](#④ Spring Retry)

① Redis--消息队列

① 发布-订阅模式

② 发布-订阅常见的模型

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

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

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

③ 发布-订阅redis命令

④ 发布-订阅模式redis实战

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

java 复制代码
# 先订阅到一个频道
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"

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

java 复制代码
# 订阅者订阅频道
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"

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

java 复制代码
# 订阅者们订阅频道
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]获取

java 复制代码
// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"
java 复制代码
// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"
java 复制代码
// 例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会校验缓存中是否有,有就执行缓存的。

java 复制代码
## 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

java 复制代码
// 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
java 复制代码
// 执行命令
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 
java 复制代码
## Redis客户端执行
127.0.0.1:6379> set userName zhangsan 
OK
127.0.0.1:6379> get userName
"zhangsan"

Demo 2

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

java 复制代码
// 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;
java 复制代码
## LimitIP:127.0.0.1为key, 10 3表示:同一IP在10秒内最多访问三次
## 前三次返回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

java 复制代码
# Redis数据库地址 
spring.redis.host=127.0.0.1 
# Redis端口 
spring.redis.port=6379 
# Redis密码(如果没有密码不用填写) 
spring.redis.password= 
java 复制代码
// 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
java 复制代码
// 在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; 
 } 
java 复制代码
// 实现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()); 
    } 
 } 
java 复制代码
//测试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

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

java 复制代码
@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("注解式");
    }
}
相关推荐
m0_7482365812 分钟前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat
web1511736022318 分钟前
Spring Boot项目中解决跨域问题(四种方式)
spring boot·后端·dubbo
我就是我35232 分钟前
记录一次SpringMVC的406错误
java·后端·springmvc
向哆哆35 分钟前
Java应用程序的跨平台性能优化研究
java·开发语言·性能优化
ekkcole1 小时前
windows使用命令解压jar包,替换里面的文件。并重新打包成jar包,解决Failed to get nested archive for entry
java·windows·jar
翱翔-蓝天1 小时前
Spring Boot 3 集成 RabbitMQ 实践指南
spring boot·rabbitmq·java-rabbitmq
luckilyil1 小时前
RabbitMQ学习—day6—springboot整合
spring boot·rabbitmq·java-rabbitmq
卑微的小鬼1 小时前
Go 语言结合 Redis 实现固定窗口、滑动窗口、令牌桶和漏桶限流算法的示例代码
开发语言·redis·golang
handsomestWei2 小时前
java实现多图合成mp4和视频附件下载
java·开发语言·音视频·wutool·图片合成视频·视频附件下载
全栈若城2 小时前
03 Python字符串与基础操作详解
java·开发语言·python