微服务项目第5天-消息中间件RabbitMQ,其中部分资料来源于十次方课程讲义。
1、消息中间件 RabbitMQ介绍
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题实现高性能,高可用,可伸缩和终一致性[架构] 使用较多的消息队列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketM
2、架构图
3、主要概念
RabbitMQ Server: 也叫broker server,它是一种传输服务。 他的角色就是维护一条 从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。
Producer: 消息生产者,如图A、B、C,数据的发送方。消息生产者连接RabbitMQ服 务器然后将消息投递到Exchange。
Consumer: 消息消费者,如图1、2、3,数据的接收方。消息消费者订阅队列, RabbitMQ将Queue中的消息发送到消息消费者。
Exchange: 生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个 或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange有 direct、fanout、topic、headers四种类型,每种类型对应不同的路由规则。
Queue:(队列)是RabbitMQ的内部对象,用于存储消息。消息消费者就是通过订阅 队列来获取消息的,RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并终 投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个 Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者 都收到所有的消息并处理。
RoutingKey: 生产者在将消息发送给Exchange的时候,一般会指定一个routing key, 来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联 合使用才能终生效。在Exchange Type与binding key固定的情况下(在正常使用时一 般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过 指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255 bytes。
Connection: (连接): Producer和Consumer都是通过TCP连接到RabbitMQ Server 的。以后我们可以看到,程序的起始处就是建立这个TCP连接。
Channels: (信道): 它建立在上述的TCP连接中。数据流动都是在Channel中进行 的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
VirtualHost: 权限控制的基本单位,一个VirtualHost里面有若干Exchange和 MessageQueue,以及指定被哪些user使用
4、安装注意事项
- Eralng这个是rabbitMQ 的开发语言(相当于环境)
- 配套软件中提供rabbitmq-server-3.7.4.exe的安装路径不能有中文,不能有空格
5、三种模式
三种模式:直接模式、分裂模式、主题模式
5.1 直接模式
每个端口的监听器会依次拿到消息
5.2 分裂模式
5.3 主题模式
图示解释:
消费者每次只能拿到一个消息,如果已经拿到消息,就不会接受下边的生产者发送的消息。
- exchange:交换机,用于(或者根据规则)绑定队列,直接指向队列
- routingKey:队列名称(其实就是查找交换对应的一个key)
- 直接模式的时候填写队列名
- 分裂模式的时候可以不填写
- 主题模式的时候填写规则
备注:
- 交换器说到底是一个名称与队列绑定的列表。当消息发布到交换器时,实际上是由你所连接的信道,将消息路由键同交换器上绑定的列表进行比较,后路由消息。
- 任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的 Queue上
6、普通户用和后台用户
7、手机验证码整体流程
整体篇幅一共两个模块:
(1)用户注册模块
(2)发短信消息监听模块
- 用户注册模块中:使用工具创建随机生成的6位短信验证码
- 存入缓存(用于注册时候的消息比对)
- 存入消息队列(用于使用阿里云工具监听进行给用户发短信)
- 给用户发一份
/**
* 发送短信验证码
* @param mobile
*/
public void sendSms(String mobile){
//生成随机6位数
String checkcode = RandomStringUtils.randomNumeric(6);
//向缓存中存一分
redisTemplate.opsForValue().set("checkcode_" + mobile, checkcode,6, TimeUnit.HOURS);
//给用户发一份
// 把电话和验证码封装成一个map.(先往消息队列中放)
Map map = new HashMap();
map.put("mobile", mobile);
map.put("checkcode", checkcode);
//做用户加密登录的时候暂时先把消息队列注释掉,直接用控制台的验证码
rabbitTemplate.convertAndSend("sms",map);
//在控制台打印一份(方便测试)
System.out.println("验证码为:" + checkcode);
}
- 创建一个map,封装电话和验证码,
- 放入队列等待消费,即等待使用工具发送给用户
- 控制台打印一份(做测试)
- 创建单独一个模块–短信消息监听模块
- 消息队列的消息要消费掉
- 监听到map之后使用阿里云短信发送工具将验证码发送给用户
短信监听模块导包:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.2.5</version>
</dependency>
</dependencies>
配置文件:(application.yml)
server:
port: 9009
spring:
application:
name: tensquare-sms #指定服务名
rabbitmq:
host: 192.168.197.129
aliyun: #自己在阿里云官网上买
sms:
accessKeyId: xxx
accessKeySecret: xxx
template_code: xxx
sign_name: xxx
监听类:
import com.aliyuncs.exceptions.ClientException;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import tensquare.utils.SmsUtil;
import java.util.Map;
@Component //作为保证能被扫描到的组件
@RabbitListener(queues = "sms")
public class RabbitListenerSms {
@Autowired
private SmsUtil smsUtil;
//这个测试的模板和签名写在配置文件中
//spring的EL表达式 (也是拿配置文件中值的方法),这个测试只有一个,方便使用
@Value("${aliyun.sms.template_code}")
private String template_code;
@Value("${aliyun.sms.sign_name}")
private String sign_name;
@RabbitHandler
public void listen(Map<String ,String> map){
String mobile = map.get("mobile");
String checkcode = map.get("checkcode");
System.out.println("手机号:" + map.get("mobile"));
System.out.println("验证码:" + map.get("checkcode"));
try {
smsUtil.sendSms(mobile,template_code,sign_name,"{\"checkcode\":\""+checkcode+"\"}");
//param中的 key 要对应网上申请写的模板中的参数;
// 表示把从队列中拿到的checkcode封装放到模板的checkcode中然后发送短信
} catch (ClientException e) {
e.printStackTrace();
}
}
}
短信工具类:(可以在阿里云上找)
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 短信工具类
* @author Administrator
*
*/
@Component
public class SmsUtil {
//产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
//产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";
//把配置文件中所有的信息拿到
@Autowired
private Environment env;
// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
/**
* 发送短信
* 一个通行证可以对应多个签名和模板,所以就在这当做参数就可以
* 如果只想用一个的话,放到配置文件中(像讲义中的一样)写死也可以,这个测试写在配置文件中方便拿取
* @param mobile 手机号
* @param template_code 模板号
* @param sign_name 签名
* @param param 参数
* @return
* @throws ClientException
*/
public SendSmsResponse sendSms(String mobile,String template_code,String sign_name,String param) throws ClientException {
String accessKeyId =env.getProperty("aliyun.sms.accessKeyId");
String accessKeySecret = env.getProperty("aliyun.sms.accessKeySecret");
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(mobile);
//必填:短信签名-可在短信控制台中找到
request.setSignName(sign_name);
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode(template_code);
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam(param);
//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
//request.setSmsUpExtendCode("90997");
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
return sendSmsResponse;
}
public QuerySendDetailsResponse querySendDetails(String mobile,String bizId) throws ClientException {
String accessKeyId =env.getProperty("accessKeyId");
String accessKeySecret = env.getProperty("accessKeySecret");
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象
QuerySendDetailsRequest request = new QuerySendDetailsRequest();
//必填-号码
request.setPhoneNumber(mobile);
//可选-流水号
request.setBizId(bizId);
//必填-发送日期 支持30天内记录查询,格式yyyyMMdd
SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd");
request.setSendDate(ft.format(new Date()));
//必填-页大小
request.setPageSize(10L);
//必填-当前页码从1开始计数
request.setCurrentPage(1L);
//hint 此处可能会抛出异常,注意catch
QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request);
return querySendDetailsResponse;
}
}
- 用户注册模块
- 从缓存中拿到短信验证码和用户输入的验证码比对
- 成功则进行用户信息入库(save)
/**
* 用户注册
* @param code 短信验证码
* @param user 用户信息
* @return
*/
@RequestMapping(value = "/register/{code}",method= RequestMethod.POST)
public Result regist(@PathVariable String code,@RequestBody User user){
//得到缓存中的验证码
String checkcodeRedis = (String) redisTemplate.opsForValue().get("checkcode_" + user.getMobile());
if (checkcodeRedis.isEmpty()){
return new Result(true,StatusCode.OK,"请先获取手机验证码!");
}
if (!code.equals(checkcodeRedis)){
return new Result(true,StatusCode.OK,"请输入正确的验证码!");
}
//下次使用消息队列中的验证码进行消费
userService.add(user); //对用户信息的设置写在service层,也可以写在controller层;
return new Result(true,StatusCode.OK,"注册成功!");
}