前言:
今天写Day10的内容,主要介绍下spring-task,WebSocket以及他们的应用案例。另外,在处理超时订单那,还会尝试用redis进行一个优化,减轻对数据库的查询压力
今日所学:
- spring-task
- 订单超时处理(优化)
- WebSocket
- 用户下单和催单提醒
ps:用户下单功能实现要用到微信支付功能,而微信支付未解决的可以看下我写的这篇博客(纯后端解决):苍穹外卖-微信支付功能解决-CSDN博客
目录
1. spring-task
Spring Task是Spring框架提供的任务调度工具
定位:定时任务框架
作用:定时的自动执行某段Java代码
从上面三句话我们知道spring-task是定时工具。那么如何确定具体的定时呢?
这里我们就需要用到Cron表达式
Cron表达式
Cron表达式是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或者7个域,每个域代表着一个含义
每个域的含义分别为:秒,分钟,小时,日,月,周,年(可选)
当然这个表达式我们不需要自己写,登录以下网站:
需要怎么设置定时,自己勾选即可,网站会自动生成相应的Cron表达式
spring-task使用流程
- 导入maven坐标(spring-context已存在)
spring-task没有单独的依赖,是包含在context里面的
2.启动类添加注解@EnableScheduling
3.自定义定时任务类
要作为一个bean交给IOC容器管理,@Component不要忘记添加
方法上加@Scheduled注解, 注解中写Cron表达式
2.订单超时处理(优化)
问题目标:
用户下单后可能存在的情况:
1.下单后未支付,订单一直处于“待支付”状态
2.用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态
对于上面两种情况需要通过定时任务来修改订单状态,具体要求为:
- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
- 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则改为完成
优化:
原本的代码是不断查询数据库,是否存在超时状态的订单。
我这边给进行了一个优化,使用了redis储存订单信息,只有当真正存在超时订单时,才会去修改数据库相应信息,以此减轻了对数据库的压力
这里我们看下运行效果
运行效果:
如图,只有当出现超时订单时,才会去查询数据库并修改相应的数据,否则每分钟的定时查询只会停留在redis层,这样大大减轻了数据库的压力
技术实现思路:
1.定义两种redis的数据结构:
- Hash结构,用来储存订单状态,判断是否支付(具体结构为 "key" :{orderId :status}
- Zset结构(有序集合) 用来储存自订单下单后15分钟的时间点(具体结构为"key": orderId-score(这边score表示下单后15分钟的时间点))
2. 在用户下单是创建这两种数据结构并相应的数据(orderId, status, Time)添加进去
3.如果支付完成(在这表现为调用payment方法),我们将Hash结构的status更新,并删除Zset结构中相应的订单,否则不变
4.如果一直没有支付到下单后15分钟,则查找相应的订单,根据订单号删除Hash结构和Zset结构中相应的数据
5.最后将订单号传给Task层,如果订单号不为空,也就是确实存在相应超时的订单,才会调用mapper层修改数据库相应的订单状态
代码实现:
1.先准备好相应的redis序列化结构
这里我在redis中定义了两个bean,第一个是用于处理JSON数据的,第二个是专门为我们这个接口设计的,用来处理long结构数据(一定要加,不然会有Integer和long数据不兼容的问题,这个问题我改了很久)
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // 设置key的序列化方式 StringRedisSerializer keySerializer = new StringRedisSerializer(); template.setKeySerializer(keySerializer); // 设置value的序列化方式,这里以JSON为例 Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setValueSerializer(valueSerializer); // 设置Hash的key和value的序列化方式 template.setHashKeySerializer(keySerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } // 下面这一个是为我们这个接口设计的,你原先的配置类不用动 @Bean public RedisTemplate<String, Long> longRedisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Long> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用StringRedisSerializer序列化key template.setKeySerializer(new StringRedisSerializer()); // 使用GenericToStringSerializer序列化value为Long template.setValueSerializer(new GenericToStringSerializer<>(Long.class)); // 对于Hash结构也做同样配置 template.setHashKeySerializer(new GenericToStringSerializer<>(Long.class)); template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class)); template.afterPropertiesSet(); return template; } }
2. 给bean注入到相应的service层
这边在提醒下第二个bean写好后一定要记得注入,再下面定义的两个常量是用来做key值的
3. 在用户下单接口创建Hash结构和Zset结构,并添加相应的订单数据
下面是monitorPaymentTimeout方法的具体方法体
可以看到定义了Zset和Hash两种结构,并添加了相应数据
这里为了方便测试,我设定的是订单创建后超过30秒未支付就为超时订单
/** * 给订单设置时间 * 监控超时订单 * @param orderId */ public void monitorPaymentTimeout(Long orderId){ // 计算下单后15分钟后面的时间戳(score) double expireTime = System.currentTimeMillis() + 0.5 * 60 * 1000; longRedisTemplate.opsForZSet().add(ORDER_PAYMENT_TIMEOUT_ZSET, (Long)orderId, expireTime); longRedisTemplate.opsForHash().put(ORDER_STATUS,orderId,Orders.PENDING_PAYMENT); }
4.如果用户支付了(这里表现为执行payment方法),更新redis相应的数据
这里我贴心的给代码粘贴放在这儿了。
具体代码逻辑就是在Hash结构中把status设置为已支付,并且删除Zset结构相应的订单信息
// 2. 更新 Redis 状态 longRedisTemplate.opsForHash().put(ORDER_STATUS, orders.getId(), Orders.PAID); // 3. 从 ZSET 移除(避免被超时任务处理) longRedisTemplate.opsForZSet().remove(ORDER_PAYMENT_TIMEOUT_ZSET, orders.getId());
5,订单超时为支付,处理超时订单
具体代码逻辑:
1.计算当前时间
2.获得时间上小于当前时间的订单(订单添加时添加的都是创建后15分钟的时间)
3.遍历获得的每个订单,查看获取到的订单的状态(status)
4.如果状态是未支付,则删除Zset结构和Hash结构相应的订单,并将相应的订单号加到result中(表示需要更新数据库的订单号)
5.返回result
/** * 处理超时订单 * @return */ @Override public List<Long> checkExpiredOrders(){ // 存放需要更改数据库状态的订单号 List<Long> result = new ArrayList<>(); //计算当前时间 long now = System.currentTimeMillis(); // 获取所有下单时间超过15分钟的订单 Set<Long> expiredOrders = longRedisTemplate.opsForZSet().rangeByScore(ORDER_PAYMENT_TIMEOUT_ZSET, 0, now); if (expiredOrders != null && !expiredOrders.isEmpty()) { expiredOrders.forEach(orderId -> { Integer status = (Integer) longRedisTemplate.opsForHash().get(ORDER_STATUS, orderId); if(status == Orders.PENDING_PAYMENT){ // Zset移除已移除的订单 longRedisTemplate.opsForHash().delete(ORDER_STATUS, orderId); longRedisTemplate.opsForZSet().remove(ORDER_PAYMENT_TIMEOUT_ZSET, expiredOrders); result.add( orderId); } } ); } return result; }
6.最后来到OrderTask层,如果checkExpiredOrders()方法传来的result不为空,也就是存在需要更改数据库的订单,我们再调用mapper执行相应的数据库逻辑
@Autowired private OrderService orderService; /** * 处理超时订单 */ @Scheduled(cron = "1/5 * * * * *") public void processTimeoutOrder() { log.info("定时处理超时订单:{}", LocalDateTime.now()); List<Long> orderIdLists = orderService.checkExpiredOrders(); // 出现超时订单 if(!orderIdLists.isEmpty() && orderIdLists != null){ for(Long orderId : orderIdLists){ Orders order = orderMapper.getByIdL(orderId); order.setStatus(Orders.CANCELLED); order.setCancelReason("订单超时"); order.setCancelTime(LocalDateTime.now()); orderMapper.update(order); } }}
3.WebSocket
定义
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信-----浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
与http的对比
Http协议和WebSocket协议的对比:
不同点:
http是短连接,即每次要进行交互都要重新发送请求,建立连接
WEbSocket是长链接,即链接后可以进行多次交互
http通信是单向的,基于请求响应模式
webSocket支持双向通信
相同点:
底层都是TCP连接的
一句话总结,虽然http和websocket底层都是TCP连接的,但是http是单向的,每次需要交互时只能由客户端重新发起请求给客户端(这种也叫短连接)。而websocket是双向的,只要建立起连接,客户端可以请求服务端,服务端也可以请求客户端(这种也叫长连接)。
应用场景:
- 视频弹幕
- 网页聊天
- 体育实况更新
还有股市等众多应用场景,这些都有一个共同的特点,即:页面没有刷新却能自动更新, 服务端主动向客户端发送消息
4.WebSocket应用-用户催单/下单
接口功能设计:
通过WebSocket实现管理端和服务端保持长连接
用户点击催单后,调用WebSccket的相关API实现服务端向客户端推送信息
客户端解服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语言播报。
约定服务端发送客户端浏览器的数据格式为JSON,字段包括: type,orderId, content
type 为消息类型,1为来单提醒 2为客户催单
- orderId 为订单id
- content 为消息内容
代码实现:
1.WebSocket环境准备
pom文件中导入相应的依赖项(项目已经导入好了)
在config层配置WebSocketConfiguration
/** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
创建一个websocket软件包,配置好WebSocketServer类。
这里代码太多,我就不粘贴了,黑马资料中有
2.实现用户下单提醒功能
具体代码逻辑:
1.先是收到小程序的支付成功通知,将相应的订单号添加到一个map键值对中。
2.将其转换成json格式的数据
3. 有websocketserver调用sendtoallclient方法将数据由服务端发送到商家端
4.最后商家端接收到相应的数据发出下单提醒并进行语言播报
这里要注意的是,如果不想语言播报一直响,记得给下面代码注释掉
3.实现催单功能
具体逻辑是一样的,这里我就不多赘述了
user端的OrderController下写好相应的Controller层
/** * 催单 * @param id * @return */ @GetMapping("/reminder/{id}") @ApiOperation("催单") public Result reminder(@PathVariable Long id) { orderService.reminder(id); return Result.success(); }
然后是service层
/** * 催单 * @param id */ @Override public void reminder(Long id) { Orders orders = orderMapper.getByIdL(id); if(orders == null){ throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND); } Map map = new HashMap(); map.put("type", 2); map.put("orderId", id); map.put("content","订单号" + orders.getNumber()); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); }
最后:
今天的分享就到这里。如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)