项目十二天
购物车
本地事务与分布式事务
本地事务: 指在同一个服务器当中对同一数据源进行事务操作 即对自己可以进行回滚
分布式事务: 指在多服务开发过程中 多个服务模块中相互调用对多个数据元进行事务操作,多服务之间也是一个一个的本地事务 自己回滚是不会让其他服务一起回滚的,所以出现了分布式事务的概念
事务四大特性
CAID
原子性 事务操作要么全部成功 要么全部失败回滚
一致性 多线程操作的时候 一线程修改完成后 后续线程看到的结果都是一致的
隔离性 某一线程在操作某一数据的时候 其他线程不可以对其进行操作
持久性 完成事务操作后,数据会永久性的保存到磁盘中
分布式系统概念
CAP理论
C 一致性 在一台服务器上进行修改后 几乎无延迟的就对其他服务一并同步该修改数据
A 可用性 高可用 集群
P 分区容错性 服务与服务之间信息传输时的自然灾害 如 网络波动
注意: 1. 分区容错性是一定存在不可避免的
2. 一致性和可用性是矛盾存在的 多集群下就做不到数据无延时同步
3. 保证PA/PC的前提也是要有一定的高可用/一致性来作为支撑的。不能全部舍去。
Base理论
# 基本可用原则 对CP系统的补充
允许返回给用户的时间有所加长 但是最终要给出响应
允许关闭非重要性的功能
# 软状态 对AP系统的补充
在执行异步操作的时候 允许多节点中可以存在延迟的现象。
# 强状态、硬状态
指数据实时同步。要求多个节点的数据副本都是一致的
# 最终一致性 对AP系统的补充
在访问三方的时候 中间过程是存在异步操作的 所以仅能保证最终结果是一致的(三方给出响应后)
分布式事务
XA协议
XA规范中分布式事务有AP,RM,TM组成:
TM 全局事务协调 接收RM的事务管理请求 接收每一个AP的事务处理结果 最终由自己来同一调度提交或回滚
AP 共享资源事务的第一个入接口
RM 每一个微服务的本地事务
2PC (两阶段提交)
是一个分布式事务同步提交的解决方案(相互等待)
第一阶段
AP 判定自己的RM是否可以操作成功或者失败 给TM发送对应的消息
第二阶段
TM 接收到AP的消息后 通知所有的RM都进行对应的回滚或者提交操作
# 注意:也就是TM与RM之间是通过两阶段提 交协议进行交互的.
2PC (两阶段提交)
优势
保证了数据的强一致性 ,适合对数据强一致要求很高的关键领域
劣势
同步事务阻塞机制 TM会等待所有的节点都发送消息后再发号指令进行统一提交或回滚 造成性能底下 牺牲了可用性,对性能影响较大,不适合高并发高性能场景
TCC(事务补偿)
概念: 核心思想是基于补偿机制 每一步操作都要有自己的确认和补偿方法
- Try程序:用于检查或占用提交事务时所需要的资源(同步)
- Confirm程序:各个事务组成,自动提交各自的事务,并告知事务协调器提交结果(异步)
- Cancel程序:发起调用各个服务的回滚程序(异步)
优势
相比两阶段提交,可用性更高一些
劣势
数据的一致性要差一些。TCC属于应用层的一种补偿方式,所以需要写try、confirm、cancel每一个模块的接口方法,导致代码量增多
分布式事务解决方案
基于seata实现
seata是基于TCC+2PC的封装升级 由alibba出品 但是还不够成熟
在使用seata的时候 如果是要AT模式 DB数据源必须要满足XA协议
基于消息队列实现
是最常用的一种解决方案
定义
把分布式事务拆分成本地小事务,并通过消息来驱动本事事务的执行,直到所有本地事务都执行成功为止。
SpringBoot集成定时器
导包
<!-- 集成quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
注入bean
//定义定时类
@Bean
public JobDetail printTimeJobDetail(){
return JobBuilder.newJob(QueryPoint.class)//PrintTimeJob我们的业务类
.withIdentity("DateTimeJob")//可以给该JobDetail起一个id
//每个JobDetail内都有一个Map,包含了关联到这个Job的数据,在Job类中可以通过context获取
.usingJobData("msg", "Hello Quartz")//关联键值对
.storeDurably()//即使没有Trigger关联时,也不需要删除该JobDetail
.build();
}
//定义触发器
@Bean
public Trigger printTimeJobTrigger() {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");//cron表达式
return TriggerBuilder.newTrigger()
.forJob(printTimeJobDetail())//关联上述的JobDetail
.withIdentity("quartzTaskService")//给Trigger起个名字
.withSchedule(cronScheduleBuilder)
.build();
}
定时类
public class QueryPoint extends QuartzJobBean {
@Autowired
private TaskMapper taskMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//获取tb_task任务表中,记录小于当前时间的记录
List<Task> taskList = taskMapper.findByCreateTimeLtCurrentTime(new Date());
//校验
if (taskList != null && taskList.size() > 0) {
//遍历
for (Task task : taskList) {
//发送增加积分消息到MQ中 ps:task对象的主键id是自增
rabbitTemplate.convertAndSend(RabbitMqConfig.EX_BUYING_ADDPOINTUSER, RabbitMqConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(task));
System.out.println("成功发送增加积分消息到MQ中");
}
}
}
}
项目第十三天
在线网址官方文档
https://pay.weixin.qq.com/wiki/doc/api/index.html
#微信SDK并未上传到远程仓库中,所以需要到官网下载,解压后,进入pom.xml文件处进入cmd黑窗口,使用mvn install,安装到本地mvn仓库
# 方法名 说明
microPay 刷卡支付
unifiedOrder 统一下单
orderQuery 查询订单
reverse 撤销订单
closeOrder 关闭订单
refund 申请退款
refundQuery 查询退款
downloadBill 下载对账单
report 交易保障
shortUrl 转换短链接
authCodeToOpenid 授权码查询openid
使用微信强制规范
创建com.github.wxpay.sdk包,包下创建MyConfig类 ,继承自抽象类WXPayConfig
//定义微信的核心配置类
public class MyConfig extends WXPayConfig {
@Override
//appid是微信公众账号或开放平台APP的唯一标识,在公众平台申请公众账号或者在开放平台申请APP账号后,微信会自动分配对应的appid,用于标识该应用。
String getAppID() {
return "wx8397f8696b538317";
}
@Override
//商户申请微信支付后,由微信支付分配的商户收款账号。
String getMchID() {
return "1473426802";
}
@Override
// 交易过程生成签名的密钥,仅保留在商户系统和微信支付后台,不会在网络中传播。商户妥善保管该Key,切勿在网络中传输,不能在其他客户端中存储,保证key不会被泄漏。
String getKey() {
return "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb";
}
@Override
InputStream getCertStream() {
return null;
}
@Override
IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String s, long l, Exception e) {
}
@Override
public DomainInfo getDomain(WXPayConfig wxPayConfig) {
return new DomainInfo("api.mch.weixin.qq.com",true);
}
};
}
}
//在容器中定义
@Bean
public WXPay wxPay(){
try {
return new WXPay(new MyConfig()); //指定wxpay的核心配置类
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
避免精度损失
bigDecelmal 避免钱的精度损失
BigDecimal payMoney = new BigDecimal("0.01");
BigDecimal fen = payMoney.multiply(new BigDecimal("100")); //1.00
fen = fen.setScale(0,BigDecimal.ROUND_UP); // 设置有几个小数位,四色五入
websocket
websocket 是一套协议规范 tomcat springboot rabbitmq 都对其有支持 只要满足其规范就可以使用
websocket 是TCP协议 但是 TCP协议又由遵循STOMP协议
STOMP 是一个简单文本对象传输协议
微信支付流程
1.用户下单 --> 跳转下单成功页面,选择支付方式 --> 点击微信支付 --> 后台微信下单(携带回调地址--> 跳转二维码页面(生成二维码)
2.扫码支付成功--> 响应给回调地址 --> 再次查询是否成功(双重保障) --> 发送MQ --> 修改订单状态为已支付 (完成订单)
后台微信下单前
1.判断订单是否存在
2.判断支付状态是否正常
3.判断远程调用三方支付的返回数据是否异常
# yml中定义微信支付成功后的回调地址 用${wxpay.notify_url}来赋值
wxpay:
notify_url: http://weizhaohui.cross.echosite.cn/wxpay/notify #回调地址 这是内网穿透后的地址
微信的回调地址
@RequestMapping("/notify")
public void notifyLogic(HttpServletRequest request, HttpServletResponse response){
System.out.println("支付成功回调");
try{
//输入流转换为字符串
String xml = ConvertUtils.convertToString(request.getInputStream());
System.out.println(xml);
//基于微信发送的通知内容,完成后续的业务逻辑处理
Map<String, String> map = WXPayUtil.xmlToMap(xml);
if ("SUCCESS".equals(map.get("result_code"))){
//查询订单
Map result = wxPayService.queryOrder(map.get("out_trade_no"));
System.out.println("查询订单结果:"+result);
if ("SUCCESS".equals(result.get("result_code"))){
//将订单的消息发送到mq'
Map message = new HashMap();
message.put("orderId",result.get("out_trade_no"));
message.put("transactionId",result.get("transaction_id"));
//消息的发送
rabbitTemplate.convertAndSend("", RabbitMQConfig.ORDER_PAY, JSON.toJSONString(message));
//完成双向通信,二维码页面中client.subscribe()监听此交换机来获取消息内容 out_trade_no。
rabbitTemplate.convertAndSend("paynotify","",result.get("out_trade_no"));
System.out.println();
}else {
//输出错误原因
System.out.println(map.get("err_code_des"));
}
}else{
//输出错误原因
System.out.println(map.get("err_code_des"));
}
//给微信一个结果通知,让其知道我们已经成功接收消息。 data数据是官网给的
response.setContentType("text/xml");
String data="<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
response.getWriter().write(data);
}catch (Exception e){
e.printStackTrace();
}
}
渲染二维码
<!-- 对二维码进行渲染! -->
<script type="text/javascript" th:inline="javascript">
let qrcode = new QRCode(document.getElementById("qrcode"), {
width : 240,
height : 240
});
qrcode.makeCode([[${code_url}]]);
let client = Stomp.client('ws://192.168.200.128:15674/ws');
let on_connect = function(x) {
id = client.subscribe("/exchange/paynotify", function(d) { //对双向通道的监听
alert(d.body);
let orderId = [[${orderId}]];
if (d.body == orderId) {
//跳转页面
location.href="/api/wxpay/toPaySuccess?payMoney="+[[${payMoney}]]
}
});
};
let on_error = function() {
console.log('error');
};
//设置双向通道中 MQ的账户密码和虚拟机
client.connect('webguest', 'webguest', on_connect, on_error, '/');
</script>
项目十四天
下单的同时 在
死信队列
订单超时支付的实现流程
目的 : 为了保证在创建后的订单 所有都是被处理过的
1.微信支付的流程只是其中一种立马被用户扫码支付的情况
2.未支付情况
3.网络原因,用户支付成功后,网络等原因未到达指定的回调接口
注意:超时之后,本地的订单可能因为网络原因导致未接受到微信的支付成功通知,所以需要先去查询微信真实的订单详情,以此判断是否关闭订单。
完成延迟功能的处理的3种方案【精通】
- 使用MQ的延迟队列
- 优点:非常的即时,提升了程序的扩展性
- 使用定时器来完成
- 优点: 简单
- 缺点:时间不准确
#(当精确性要求很高时使用如下方案)
- 使用定时器来筛选即将确认收货的订单,监控到这些订单之后,在放入MQ之前对这些消息设置过期时间,使用MQ的延迟队列来精准控制订单自动确认收货的准确时间 (还有十分钟需要自动签收,监控到之后对这条消息设置十分钟过期时间,在延迟队列中待满十分钟后就可以进行自动签收,去到死信队列中等待读取消息后进行签收)
- 保证及时性和避免时间不准确性
//获取当前时间
LocalDate now = LocalDate.now();
LocalDate date = now.plusDays(-orderConfig.getTakeTimeout());
常用Cron表达式
0 0 10,14,16 * * ? #每天上午10点,下午2点,4点
0 0/30 9-17 * * ? #朝九晚五工作时间内每半小时
0 0 12 ? * WED #表示每个星期三中午12点
"0 0 12 * * ?" #每天中午12点触发
"0 15 10 ? * *" #每天上午10:15触发
"0 15 10 * * ?" #每天上午10:15触发
"0 15 10 * * ? *" #每天上午10:15触发
"0 15 10 * * ? 2005" #2005年的每天上午10:15触发
"0 * 14 * * ?" #在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" #在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" #在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" #在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" #每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" #周一至周五的上午10:15触发
"0 15 10 15 * ?" #每月15日上午10:15触发
"0 15 10 L * ?" #每月最后一日的上午10:15触发
"0 15 10 ? * 6L" #每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" #2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" #每月的第三个星期五上午10:15触发
事务复习
# 脏读
事务1第二次读取时,读到了事务2未提交的数据。若事务2回滚,则事务1第二次读取时,读到了脏数据。
(在第二次读取的时候 读到了未提交的数据)
# 不可重复读
事务1第二次读取时,读到了事务2已提交的数据。
# 幻读
事务1第二次查询时,读到了事务2提交的数据。
# 不可重复读与幻读的区别
确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
不可重复读针对的是值的不同,幻读指的是数据条数的不同。
避免不可重复读需要锁行。
避免幻影读则需要锁表。
# CAID 事务特性
原子性 事务操作要么全部成功 要么全部失败回滚
一致性 多线程操作的时候 一线程修改完成后 后续线程看到的结果都是一致的
隔离性 某一线程在操作某一数据的时候 其他线程不可以对其进行操作
持久性 完成事务操作后,数据会永久性的保存到磁盘中
项目十五、六天
用户在下单的时候,需要基于JWT令牌信息进行登陆人信息认证,确定当前订单是属于谁的。
针对秒杀的特殊业务场景,仅仅依靠对象缓存或者页面静态化等技术去解决服务端压力还是远远不够。
对于数据库压力还是很大,所以需要异步下单,异步是最好的解决办法,但会带来一些额外的程序上的复杂性。
秒杀单独作为一个模块
可以在高并发的情况下 方便进行横向扩容 集群操作
秒杀得保证原子性
为了确保原子性操作,所以在redis中存储和扣减添加商品都要使用
redisTemplate.opsForValue().decreament(redis中的key)来获取 扣减或者增加后的值
独立秒杀商品表和秒杀订单表
数据库的表要单独成立 否则在高并发环境下 数据库不能够承受如此打的访问量
设置redistemplate的序列化(如果设置了 就不会走底层封装的)
//设置redistemplate的序列化
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
//秒杀商品和库存添加到缓存中
for (SeckillGoods seckillGoods : seckillGoodsList) {
//使用opsForHash.put方法追加商品
redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName,seckillGoods.getId(),seckillGoods);
//加载秒杀商品的库存。使用opsForValue().set单独添加一个key、val键值对
redisTemplate.opsForValue().set(SECKILL_GOODS_STOCK_COUNT_KEY+seckillGoods.getId(),seckillGoods.getStockCount());
}
业务层下单
//业务层
public boolean add(Long id, String time, String username) {
/**
* 1.获取redis中的商品信息与库存信息,并进行判断
* 2.执行redis的预扣减库存操作,并获取扣减之后的库存值
* 3.如果扣减之后的库存值<=0,则删除redis中响应的商品信息与库存信息
* 4.基于mq完成mysql的数据同步,进行异步下单并扣减库存(mysql)
*/
//防止用户恶意刷单,通过redis原子性递增来判断是否是恶意访问
String result = this.preventRepeatCommit(username, id);
if ("fail".equals(result)){
return false;
}
//防止相同商品重复购买
//自定义查询语句 查看数据库中是否存在该用户的相同订单
SeckillOrder order = seckillOrderMapper.getOrderInfoByUserNameAndGoodsId(username, id);
if (order != null){
return false;
}
//获取redis中的商品信息(通过定时器已经存入redis)
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SECKILL_GOODS_KEY+time).get(id);
//获取redis中库存信息(通过定时器已经存入redis)
String redisStock = (String) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY+id);
//如果未取出key直接返回
if (StringUtils.isEmpty(redisStock)){
return false;
}
//如果库存为空直接返回
int stock = Integer.parseInt(redisStock);
if (seckillGoods == null || stock<=0){
return false;
}
//执行redis的预扣减库存,并获取到扣减之后的库存值
//decrement:减 increment:加 -> Lua脚本语言
//获得衰减之后的值
Long decrement = redisTemplate.opsForValue().decrement(SECKILL_GOODS_STOCK_COUNT_KEY + id);
if (decrement<=0){
//扣减完库存之后,当前商品已经没有库存了.
//删除redis中的商品信息与库存信息
redisTemplate.boundHashOps(SECKILL_GOODS_KEY+time).delete(id);
redisTemplate.delete(SECKILL_GOODS_STOCK_COUNT_KEY + id);
}
//设置秒杀订单的必要默认项,此处省略
//发送消息(保证消息生产者对于消息的不丢失实现) 此处是自定义的方法调用!
confirmMessageSender.sendMessage("", RabbitMQConfig.SECKILL_ORDER_QUEUE, JSON.toJSONString(seckillOrder));
//异步提速返回给用户 后续由MQ来处理消息
return true; //控制层通过true/false来判断是否下单成功
}
//自定义消息发送方法
public void sendMessage(String exchange,String routingKey,String message){
//设置消息的唯一标识并存入到redis中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(correlationData.getId(),message);
//将本次发送消息的相关元数据保存到redis中
Map<String,String> map = new HashMap<>();
map.put("exchange",exchange);
map.put("routingKey",routingKey);
map.put("message",message);
redisTemplate.opsForHash().putAll(MESSAGE_CONFIRM_KEY+correlationData.getId(),map);
//携带着本次消息的唯一标识,进行数据发送
rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);
}
保证MQ中的消息可靠性
RabbitMQ提供两种持久化方式
1.事务机制
虽然能够保证数据安全,但是此机制采用的是同步机制,会产生系统间消息阻塞,影响整个系统 的消息吞吐量。从而导致整个系统的性能下降,因此不建议使用
2.confirm机制
confirm模式需要基于channel进行设置, 一旦某条消息被投递到队列之后,消息队列就会发送一个确认信息给生产者,如果队列与消息是可持久化的, 那么确认消息会等到消息成功写入到磁盘之后发出.confirm的性能高,主要得益于它是异步的.生产者在将第一条消息发出之后等待确认消息的同时也可以继续发送后续的消息.当确认消息到达之后,就可以通过回调方法处理这条确认消息. 如果MQ服务宕机了,则会返回nack消息. 生产者同样在回调方法中进行后续处理。
(依然不是百分百,磁盘会损坏)
在channel信道的传输过程中 消息是很容易丢失的 因为消息是存储到内存当中的 所以要将消息保存到磁盘中 下次再读取
秒杀的解决方案
开启持久化
spring:
rabbitmq:
host: 192.168.93.139
port: 5672
# 开启消息手动签收模式
listener:
simple:
acknowledge-mode: manual #手动签收
prefetch: 1 # 一次拉去一条 如果发生错误,就一直处理 直到解决
# 开启return机制
publisher-returns: true
# 开启confirm机制
publisher-confirm-type: correlated
//声明交换机
@Bean(EX_BUYING_ADDPOINTUSER)
public Exchange EX_BUYING_ADDPOINTUSER(){
return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTUSER).durable(true).build(); //链式调用使交换机持久化
}
//声明队列
@Bean(CG_BUYING_ADDPOINT)
public Queue CG_BUYING_ADDPOINT(){
Queue queue = new Queue(CG_BUYING_ADDPOINT,true); //在方法中定义队列的持久化
return queue;
}
//消息的持久化
rabbitTemplate是默认开启消息持久化操作的
// 注意: 当所有都开启持久化操作之后 消息仍然是不可靠的 因为在写的过程中 如果宕机 那么依然会丢失消息
// 所以还要搭配rabbitMQ的消息保护机制 事务机制和confirm机制
// 事务机制 是同步的 如果没有得到 回调消息 成功或失败 那么会一直等待 阻塞后续消息的发送
confirm回调
回调由两种方式。这是其中一种,独立一个类处理形成回调。另外一种在畅购上的cannal有体现。
首先是需要在yml中开启confirm
# 开启confirm机制
publisher-confirm-type: correlated
package com.changgou.seckill.config;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class ConfirmMessageSender implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate redisTemplate;
public static final String MESSAGE_CONFIRM_KEY="message_confirm_";
//有参构造函数
public ConfirmMessageSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setConfirmCallback(this);
}
@Override
//接收消息服务器返回的通知的
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
//成功通知
//删除redis中的相关数据
redisTemplate.delete(correlationData.getId());
redisTemplate.delete(MESSAGE_CONFIRM_KEY+correlationData.getId());
}else{
//失败通知
//从redis中获取刚才的消息内容
Map<String,String> map = (Map<String,String>)redisTemplate.opsForHash().entries(MESSAGE_CONFIRM_KEY+correlationData.getId());
//重新发送
String exchange = map.get("exchange");
String routingkey = map.get("routingkey");
String message = map.get("message");
rabbitTemplate.convertAndSend(exchange,routingkey, JSON.toJSONString(message));
}
}
//自定义消息发送方法
public void sendMessage(String exchange,String routingKey,String message){
//设置消息的唯一标识并存入到redis中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(correlationData.getId(),message);
//将本次发送消息的相关元数据保存到redis中
Map<String,String> map = new HashMap<>();
map.put("exchange",exchange);
map.put("routingKey",routingKey);
map.put("message",message);
redisTemplate.opsForHash().putAll(MESSAGE_CONFIRM_KEY+correlationData.getId(),map);
//携带着本次消息的唯一标识,进行数据发送
rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);
}
}
签收消息
有三种方式进行签收
自动、手动、异常情况确认
// yml 文件中要开启manual
rabbitmq:
host: 192.168.200.128
listener:
simple:
acknowledge-mode: manual //开启了消费者手动应答模式
// 取出消息后进行
@Component
public class ConsumerListener {
@Autowired
private SecKillOrderService secKillOrderService;
@RabbitListener(queues = RabbitMQConfig.SECKILL_ORDER_QUEUE)
public void receiveSecKillOrderMessage(Message message, Channel channel){
//设置预抓取总数
try {
channel.basicQos(300); //设置的一次签收数量 建议在100-300之间
} catch (IOException e) {
e.printStackTrace();
}
//1.转换消息格式
SeckillOrder seckillOrder = JSON.parseObject(message.getBody(), SeckillOrder.class);
//2.基于业务层完成同步mysql的操作
int result = secKillOrderService.createOrder(seckillOrder);
if (result>0){
//同步mysql成功
//向消息服务器返回成功通知
try {
/**
* 第一个参数:消息的唯一标识
* 第二个参数:是否开启批处理
*/
//签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (IOException e) {
e.printStackTrace();
}
}else{
//同步mysql失败
//向消息服务器返回失败通知
try {
/**
* 第一个参数:消息的唯一标识
* 第二个参数: 是否开启批处理, 指在本地存到一定量之后统一发送给队列
* 第三个参数:true当前消息会进入到队列,false当前的消息会重新进入到原有队列中,默认回到尾部(队列左进右出)
*/
//拒签
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
防止恶意访问
注意:
1.通信方面 由于规范原因 所以经常都是一块区域的用户对于输出的ip其实是同一个ip 所以在对ip进行限流的时候要注意这一特性。有可能会限制到大批量的用户
2.在对数据操作进行优化的时候,可以考虑移动代码的位置来控制大量的不必要逻辑判断。
比如下面的代码。
如果第一个if判断不移动位置,那么如果在大数据情况下会对redis的内存造成消耗
如果往下移动到最后作为if判断,那么可以使用redis库存过滤掉绝大多数非法请求
//防止用户恶意刷单
String result = this.preventRepeatCommit(username, id);
if ("fail".equals(result)){
return false;
}
//防止相同商品重复购买
//自定义查询语句 查看数据库中是否存在该用户的相同订单
SeckillOrder order = seckillOrderMapper.getOrderInfoByUserNameAndGoodsId(username, id);
if (order != null){
return false;
}
//获取商品信息
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SECKILL_GOODS_KEY+time).get(id);
//获取库存信息
String redisStock = (String) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY+id);
//如果未取出key直接返回
if (StringUtils.isEmpty(redisStock)){
return false;
}
//如果库存为空直接返回
int stock = Integer.parseInt(redisStock);
if (seckillGoods == null || stock<=0){
return false;
}
// 使用redis的过期时间 来判定是否恶意访问
private String preventRepeatCommit(String username,Long id){
String redis_key = "seckill_user_"+username+"_id_"+id;
//首次访问设置为1,通过原子性操作保证每次请求都递增1
long count = redisTemplate.opsForValue().increment(redis_key, 1);
//如果第一次访问 才会被认定为不是恶意访问
if (count == 1){
//代表当前用户是第一次访问.
//对当前的key设置一个五分钟的有效期
redisTemplate.expire(redis_key,5, TimeUnit.MINUTES);
return "success";
}
if (count>1){
return "fail";
}
//默认为fail
return "fail";
}
防止同商品重复秒杀
// 查询秒杀订单表 如果有数据那么拒绝再次秒杀
@Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}")
SeckillOrder getOrderInfoByUserNameAndGoodsId(@Param("username") String username, @Param("id") Long id);
隐藏重要接口
//调用
add:function (id) {
//第一次异步条用 来获取一个随机值
axios.get("/api/wseckillorder/getToken").then(function (response) {
var random =response.data;
//第二次异步调用
axios.get("/api/wseckillorder/add?time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id+"&random="+random).then(function (response) {
if (response.data.flag){
alert("抢单成功,即将进入支付");
} else{
alert("抢单失败");
}
})
})
}
@RestController
@RequestMapping("/wseckillorder")
public class SecKillOrderController {
@Autowired
private SecKillOrderFeign secKillOrderFeign;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/add")
@AccessLimit
public Result add(@RequestParam("time") String time, @RequestParam("id")Long id,String random){
//获取cookie
String cookieValue = this.readCookie();
//获取redis的key值
String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_"+cookieValue);
//逻辑判断
if (StringUtils.isEmpty(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下单失败");
}
if (!random.equals(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下单失败");
}
//下单
Result result = secKillOrderFeign.add(time, id);
return result;
}
@GetMapping("/getToken")
@ResponseBody
public String getToken(){
String randomString = RandomUtil.getRandomString();
String cookieValue = this.readCookie();
redisTemplate.opsForValue().set("randomcode_"+cookieValue,randomString,5, TimeUnit.SECONDS);
return randomString;
}
//读取cookie 重要的是 如何在spring中获取到HttpServletRequest对象
private String readCookie(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String jti = CookieUtil.readCookie(request, "uid").get("uid");
return jti;
}
}
商品限流及自定义注解
对某一个接口进行限流,此前都是对某一个服务整体限流
基于guawa来做限流
令牌桶
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
2.定义自定义注解
// 自定义注解接口
@Inherited //可被继承
@Documented
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})//定义使用的位置
@Retention(RetentionPolicy.RUNTIME) //不仅保存到class文件中,并且jvm加载class之后,该注解仍然存在
public @interface AccessLimit {
}
//自定义注解 AOP实现
@Component
@Scope
@Aspect
public class AccessLimitAop {
@Autowired
private HttpServletResponse response; //自动注入响应对象
//额外方案
1。定义concurrentMap 来当作大令牌桶
//设置令牌的生成速率
//这是一个拦截所有请求的策略
private RateLimiter rateLimiter = RateLimiter.create(2.0); //每秒生成两个令牌存入桶中
@Pointcut("@annotation(com.changgou.seckill.web.aspect.AccessLimit)") //切点规则
public void limit(){}
//定义切面 环绕
@Around("limit()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
boolean flag = rateLimiter.tryAcquire(); //判断是否有多余的令牌
Object obj = null; //返回值
//额外方案 是对所有
2.获取到请求中的用户真实地址
3.用该用户在大令牌桶中获取出自己的令牌, 判断rateLimiter.tryAcquire()是否有令牌可以取
如果则false 创建 RateLimiter.create(2.0); 存入map key是该用户标识、val是创建的令牌
如果有true 放行
if (flag){
//允许访问
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}else{
//不允许访问,拒绝
String errorMessage = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
//或者是直接返回obj,不需要再调用outMessage方法
String obj = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
//将信息返回到客户端上
this.outMessage(response,errorMessage);
}
return obj;
}
// 对拒绝的请求 进行响应
private void outMessage(HttpServletResponse response,String errorMessage){
ServletOutputStream outputStream = null;
try {
response.setContentType("application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(errorMessage.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
项目核心解决方案
1.最终一致性(积分)
2.消息可靠性(秒杀)
3.二级缓存(访问)
4.canal(商品管理)
);
}
}else{
//不允许访问,拒绝
String errorMessage = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,“fail”));
//或者是直接返回obj,不需要再调用outMessage方法
String obj = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
//将信息返回到客户端上
this.outMessage(response,errorMessage);
}
return obj;
}
// 对拒绝的请求 进行响应
private void outMessage(HttpServletResponse response,String errorMessage){
ServletOutputStream outputStream = null;
try {
response.setContentType("application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(errorMessage.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
# 项目核心解决方案
1.最终一致性(积分)
2.消息可靠性(秒杀)
3.二级缓存(访问)
4.canal(商品管理)