大家好我是霜华!!!
我就问大家一句话
你们面对大学抢课的那天
你们是否想起了被请求超时支配的恐惧
很多朋友认为是服务器的问题
这可能是是一方面,但更多的可能代码设计的问题
分布式跟消息队列以及缓存在并发量稍微高一点点的项目里实在太重要了
而消息队列能极大的减少RT的时间(一次响应的时间)
我做项目时候上传一个带图片的文章就要花500ms这延迟实在是高的恐怖
来看看这套业务
这套简单的业务大概是花了500ms左右,延迟真的是非常高的
但其实我们把数据交给服务器就可以直接response回去,剩下的转码磁盘化让服务器慢慢整
我们可以开个线程一个个的去调用这些业务,但这真的非常繁琐
而且逻辑非常容易,一套流程光找bug就得很久,而且出异常了怎么办,一个个加try catch可以,但这玩意一旦当问题积累到一定程度,就可能P0级别的事物,你可以去人事领薪资早早回家了
这时候可以用一个队列数据结构将业务流程数据一包包放进去再根据顺序一个个拿出来执行
这样我调用一个人帮我做事后,我就直接给用户response 回去,让该流程在后台慢慢运转,有些甚至是可以同时做的看看这个电商流程:
而且通过这种队列,能够在请求量大的时候,减少短时间内打在服务器上的次数,保证服务器安全
这时候大家可能会想到redis的list队列
以redis 的list队列实现的中间件,能够一定程度上解决这种异步的问题
这时候我们运用消费者生产者模式,消费者监听生产者的push
并且可以支持多个生产者和消费者并发进出消息(就是我们的线程啦)
每个消费者拿到的都是不同元素
Redis 的消息队列没有非常多的高级特性没有ack保证,如果对消息的可靠性有着极高要求请移步RocketMQ和kafka,
Redis 的list列表数据结构常用来作为异步消息队列使用,用rpush 和lpush 操作入队列,用lpop 和rpop 操作出队列
可以支持多个生产者和多个消费者并发进出消息,每个消费者拿到的消息都是不同的列表元素
list可以支持多个生产者和多个消费者并发进出消息,每个消费者拿到都是不同的列表元素
技术是把双刃剑你解决了问题同时也会出现问题:
最基本的问题就是麻烦,原本按照流程做挺舒服现在还得照顾这个是否有安全问题。照顾那个是否会爆异常。
问题:
1.队列空了怎么办
原本客户端通过循环pop操作来获取信息,然后进行处理,处理完了再接着获取消息再进行处理,如此循环,这便是作为消息队列消费者客户端的生命周期。、
如果队列空留,就好pop死循环,拉高CPU消耗,和Redis的QPS(每秒查询率)
这时候加个判断如果pop出null 让线程睡一会
2.延迟高
阻塞读:
上面的解决有问题,如果睡了1s,那如果突然有新的数据呢,延迟就是1s
阻塞读blocking就是一个很好的解决方法
阻塞读在队列没有数据的时候,会立即进入休眠,一旦数据到来,立刻醒过来,延迟几乎为0,用blpop/brpop替代lpop/rpop真的很丝滑哦
3.空闲连接自动断开:
如果线程一直在睡着阻塞这,Redis客户端就形成闲置连接,闲置过久服务器主动断开这时候在存取就会出现问题
所以务必自己封装消费者加些异常处理机制
4.锁冲突处理:
客户端在拿到消息后处理请求时没加锁成功,安全起见这时候是不能执行
三种方法解决:
(1)直接抛出异常让用户重新发起请求
(2)让消费者sleep睡一觉,形成阻塞,但不适用队列很多的情况
(我们这忙的热火朝天,你在这睡觉?还占这茅坑?)
(3)延时队列 将当前队列扔到另一个队列延后处理,以避开冲突
注意:
你交给消息队列的消息一定是对当前response没影响的
其实redis呢已经帮我们封装好了队列需要的生产者和消费者我们一起来看看怎么用吧。
@Configuration
public class RedisConfig {
//这是一个Redis相关的配置类
private final static Logger logger=LoggerFactory.getLogger(RedisConfig.class);//一个日志类,我想监听一下类,通过日志看看
@Autowired
private RedisLister redisListener;
@Bean//配置下RedisTemplate(相当于Jedis)
public RedisTemplate <String,Object>
redisTemplate(RedisConnectionFactory factory ){
//设置下序列化
logger.debug("redis序列化开始");
RedisTemplate<String,Object> template=new RedisTemplate<>();
template.setConnectionFactory(factory);//把template放到容器中
//String 序列化
RedisSerializer serializer= new GenericJackson2JsonRedisSerializer();
//设置默认序列化方式
template.setDefaultSerializer(serializer);
//配置的默认的序列化方式为GenericJackson2JsonRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
//配置键的序列化方式为StringRedisSerializer
template.setHashKeySerializer(serializer);
//我们配置哈希表的值的序列化方式为GenericJackson2JsonRedisSerializer
logger.debug("redis序列化结束");
return template;
}
@Bean//讲监听器,加到redis队列的监听器容器中
public RedisMessageListenerContainer container(RedisConnectionFactory factory){
RedisMessageListenerContainer container=new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(redisListener,new PatternTopic("demo-channel"));
//新建一个redis监听器容器,监听器和管道名绑定,最后返回这个容器
return container;
}
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory factory){
return new RedisLockRegistry(factory,"demo-lock",60);
//RedisLockRegistry的构造函数,第一个参数是redis连接池,第二个参数是锁的前缀,即取出的锁,键名为“domo-lock:KEY-NAME”
//第三个参数为锁的过期时间(秒)默认为90s 当持有锁超过该时间后自动过期
}
}
//redis推理推送服务
@Service
public class Publisher {
@Autowired
private RedisTemplate redisTemplate;
public void publish(Object msg){
redisTemplate.convertAndSend("demo-channel",msg);
//向某个通道(参数1)推送一跳消息
//生产者消费者通道名要相同
}
}
//redis队列监听器
@Component
public class RedisLister implements MessageListener {
@Autowired
private RedisLockRegistry redisLockRegistry;
private static final Logger LOGGER=LoggerFactory.getLogger(RedisLister.class);
@Override
public void onMessage(Message message, byte[] pattern) {
Lock lock=redisLockRegistry.obtain("lock");
//方法师监听到队列中消息后回调方法
//message:redis消息类,该类仅有两个方法,byte[] getBody()以二进制形式获取消息体,byte getChannel 以二进制形式获取消息通道
//pattern 二进制形式消息通道,和message.getChannel()返回值相同
try {
lock.lock();//上锁,处理分布式问题
LOGGER.debug("从消息通道={}监听到消息",new String(pattern));
LOGGER.debug("从消息通道={}监听到消息",new String(message.getChannel()));
LOGGER.debug("元消息={}",new String(message.getBody()));
// 新建一个用于反序列化的对象,注意这里的对象要和前面配置的一样
// 因为我前面设置的默认序列化方式为GenericJackson2JsonRedisSerializer
// 所以这里的实现方式为GenericJackson2JsonRedisSerializer
RedisSerializer serializer=new GenericJackson2JsonRedisSerializer();
LOGGER.debug("反序列化后的消息={}",serializer.deserialize(message.getBody()));
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
原码来自于敖丙:
什么鬼,面试官竟然让敖丙用Redis实现一个消息队列!!?
https://mp.weixin.qq.com/s/5NOTLJ6AM3QJfhvXMSR-MA
参考书:
Redis深度历险核心原理与应用实践 (钱文品/著)