Redis队列自研组件

     背景

        年初的时候设计实践过一个课题:SpringBoot+Redis实现不重复消费的队列,并用它开发了一个年夜饭下单和制作的服务。不知道大家还有没有印象。完成这个课题后,我兴致勃勃的把它运用到了项目里面,可谁曾想,运行不久后运维人员就找到了我,说我的功能有问题,而且问题很大。具体表现在用户下单之后,很久都没有收到订单完成的消息,后台服务里的日志也是残缺的,只查到了开始制作年夜饭的日志,没有年夜饭制作过程和完成的日志。

        为此,我花了大量的时间和精力去分析程序,定位问题,最终发现,是运维人员在系统上线时将生产服务和UAT服务的redis服务地址给配成了同一个,这就导致生产上的订单进入Redis队列后,被UAT服务给消费了,而UAT和生产又是不同的数据库,自然导致UAT上通过队列中的主键在数据库中找不到相关数据,从而消费失败的结果。而这些失败的信息全都记录在了UAT的服务器上,生产服务器中自然很难分析和定位到问题。

        要解决这个问题其实很简单,只需要把channelTopic改为从配置文件中获取,然后生产、UAT环境配置不同的字符串即可。

        但实际上真的只做这些就够了吗?如果下次再发生不可预料的问题,我还要花那么多的时间吭哧吭哧的去看日志,调程序,定位问题吗?答案是否定的!

      原服务整改

        结合生产环境产生的问题,痛定思痛,我决定对原来的服务进行一轮大整改,优化服务的可维护性,可测试性。在我的构想中,新的服务需要有以下功能:

        1、保证原有的“年夜饭”功能稳定正常的运行。

        2、可以查询哪些订单还未开始处理

        3、可以查询哪些订单已经处理,以及处理结果。

        4、可以清空N天以前的处理成功的订单。

        5、可以清空待处理的订单

        6、对于已经处理但处理失败的订单,可以一键重新处理

        7、待处理订单插队

        我的设想是,通过redisTemplate.opsForZSet()方法创建两个新队列:待办队列和已办队列,在下单时,插入一条数据到待办队列,在处理任务时,从待办队列中删除该数据,在处理完成后,插入一条数据到已办队列,这样,通过查询待办队列,已办队列,就可以知道哪些任务还在排队,哪些任务已经完成了。

        以下是我的程序整改过程,老粉可以对比上一篇博客看看两者的不同之处。

#以下是application.yml配置server:port: 19200servlet:context-path: /leiximax-http-header-size: 102400
spring:redis:database: 9host: 127.0.0.1port: 6379password:jedis:pool:max-active: 8max-wait: -1max-idle: 8min-idle: 0
leixi:redis-queue-key: NEW_YEAR_DINNER_DEV
//以下是Java程序代码:
/*** Redis配置** @author leixiyueqi* @since 2024/06/15 22:00*/
@Configuration
public class RedisConfig {@Value("${leixi.redis-queue-key}")private String REDIS_QUEUE_KEY ;@Beanpublic ChannelTopic topic() {return new ChannelTopic(REDIS_QUEUE_KEY+ Constant.WORKING_QUEUE_SUFFIX);}@Beanpublic MessageListenerAdapter messageListenerAdapter(DinnerListener listener) {return new MessageListenerAdapter(listener);}@Beanpublic RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,MessageListenerAdapter messageListenerAdapter,ChannelTopic topic) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(redisConnectionFactory);container.addMessageListener(messageListenerAdapter, topic);return container;}
}/*** 订单处理控制层* * @author leixiyueqi* @since 2024/06/15 22:00*/
@RestController
public class DinnerController {private int i = 0;@Autowiredprivate DinnerService service;@Value("${leixi.redis-queue-key}")private String REDIS_QUEUE_KEY ;@GetMapping("/orderDinner")public Object orderDinner() {OrderEntity entity = new OrderEntity();entity.setOrderCode("Order" + (++i));entity.setCustomerName("第"+i+"位客户");return service.orderNewYearEveDinner(entity);}@Autowiredprivate RedisTemplate<String, String> redisTemplate;@GetMapping("/getPendingOrder")public Object getPendingOrder() {return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);}@GetMapping("/cleanPendingOrder")public Object cleanPendingOrder() {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);if(set.size() > 0) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY + Constant.PENDING_SUFFIX, set.toArray());}return "待处理订单已被清空!";}@GetMapping("/getHandledOrder")public Object getHandledOrder() {return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, -1);}@GetMapping("/cleanOldSucceedOrder")public Object cleanHandledOrder(@RequestParam("day") Integer day) {Set<String> set = redisTemplate.opsForZSet().rangeByScore(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, Constant.getScoreByDate() - day);set.forEach(s -> {JSONObject obj = JSONObject.parseObject(s);if (obj.getString("result").equals("SUCCESS")) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, s);}});return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, System.currentTimeMillis());}/*** 这里还有最后一个问题, 把已办里的错误的信息摘除出来,重新走请求。并且反馈哪些信息重新走了请求。*/@GetMapping("/restartFailedOrder")public Object restartFailedOrder() {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, -1);StringBuilder sb = new StringBuilder();set.forEach(s -> {JSONObject obj = JSONObject.parseObject(s);if (!obj.getString("result").equals("SUCCESS")) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, s);OrderEntity entity = JSON.parseObject(obj.getString("msg"), OrderEntity.class);service.orderNewYearEveDinner(entity);sb.append(entity.getOrderCode()).append(",");}});return "以下订单号被重启: "+ sb;}@GetMapping("/cutInLineJob")public Object cutInLineJob(@RequestParam("orderCode")  String orderCode) {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);for (String s : set) {OrderEntity obj = JSONObject.parseObject(s, OrderEntity.class);if (obj.getOrderCode().equals(orderCode)) {CompletableFuture.runAsync(() -> {service.doListenerWork(s);});return "订单 " + orderCode + " 插队成功!";}}return " 插队失败,该订单已经在制作了!";}
}/**** @author leixiyueqi* @since 2024/06/15 22:00*/
@Data
public class OrderEntity implements Serializable {/*** 客户姓名*/private String customerName;/*** 订单号*/private String orderCode;/*** 菜单*/List<String> menus;/*** 出餐状态*/private String dinnerState;/*** 做饭开始时间*/private String dinnerStartTime;/*** 做饭结束时间*/private String dinnerEndTime;/*** 备注*/private String remark;
}/*** 监听类*  * @author leixiyueqi* @since 2024/06/15 22:00*/
@Component
public class DinnerListener implements MessageListener {@Autowiredprivate DinnerService service;private final Object lock = new Object();@Overridepublic void onMessage(Message message, byte[] pattern)  {synchronized (lock) {service.doListenerWork(message.toString());}}
}/**** @author leixiyueqi* @since 2024/06/15 22:00*/
@Slf4j
@Service
public class DinnerService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Value("${leixi.redis-queue-key}")private String REDIS_QUEUE_KEY ;/*** 年夜饭下单** @param req 订单信息* @return*/public Object orderNewYearEveDinner(OrderEntity req) {// 存储订单信息saveOrder(req);// 异步开始做菜redisTemplate.delete(JSON.toJSONString(req));redisTemplate.opsForZSet().add(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, JSON.toJSONString(req), Constant.getScoreByDate());redisTemplate.convertAndSend(REDIS_QUEUE_KEY+ Constant.WORKING_QUEUE_SUFFIX, JSON.toJSONString(req));return "您已成功下单,订单号为"+ req.getOrderCode()+",后厨正在准备预制菜!";}/*** 这里模拟的是做年夜饭的过程方法,该方法用时较长,整个过程需要10秒,但是,这个过程中存在多种意外,该方法可能失败** @param req 订单信息*/public void doNewYearEveDinner(OrderEntity req) throws Exception {System.out.println("开始做订单 " + req.getOrderCode() + " 的年夜饭");Thread.sleep(10000);// 这里写个方法模拟报错的场景int i = new Random().nextInt(6) + 1;if (i ==4) {throw new Exception("厨师跑了");}if (i ==5) {throw new Exception("食物跑了");}if (i ==6) {throw new Exception("厨房着火了");}System.out.println("订单 " + req.getOrderCode() + " 的年夜饭已经完成");}private void saveOrder(OrderEntity req) {//这里假设做的是订单入库操作System.out.println("订单 " + req.getOrderCode() + " 已经入库, 做饭开始时间为 "+ new Date());}/*** 根据订单编号修改订单信息** @param orderCode 订单编号* @param dinnerStatus* @param remark*/public void updateOrder(String orderCode, String dinnerStatus, String remark) {// 根据订单编号修改订单的出餐结束时间,出餐状态,备注等信息。System.out.println("更新订单 "+ orderCode +" 信息,做饭结束时间为 "+ new Date() + ", 出餐状态为"+ dinnerStatus +", 备注为 " +remark);}public void doListenerWork(String message) {Boolean flag = redisTemplate.opsForValue().setIfAbsent(message, "1", 1, TimeUnit.DAYS);// 加锁失败,已有消费端在此时对此消息进行处理,这里不再做处理if (!flag) {return;}redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, message);OrderEntity param = CastUtils.cast(JSON.parseObject(message, OrderEntity.class));JSONObject obj = new JSONObject();obj.put("msg", message);try {obj.put("server", InetAddress.getLocalHost().getHostAddress());this.doNewYearEveDinner(param);this.updateOrder(param.getOrderCode(), "SUCCESS", "成功");obj.put("result", "SUCCESS");}catch (Exception e) {e.printStackTrace();this.updateOrder(param.getOrderCode(), "FAIL", e.getMessage());obj.put("result", "FAIL");obj.put("desc", e.getMessage());}finally {obj.put("endTime", new Date());redisTemplate.opsForZSet().add(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, obj.toJSONString(), Constant.getScoreByDate());}}/*** 静态工具类* * @author leixiyueqi* @since 2024/06/15 22:00*/
public class Constant {// 工作队列后缀public static final String WORKING_QUEUE_SUFFIX = "_QUEUE";//待处理工作队列后缀public static final String PENDING_SUFFIX = "_PENDING";// 已处理工作后缀public static final String HANDLED_SUFFIX = "_HANDLED";//一天的毫秒数private static final Integer ONE_DAY_MINI = 86400000;/*** 根据当前日期计算队列的分数** @return*/public static Integer getScoreByDate() {return (int)System.currentTimeMillis()/ONE_DAY_MINI;}
}

      接口测试

        1、年夜饭下单

        

        2、查询待处理订单

        

        3、查询已处理订单

        

        4、清空已处理且成功的订单

        

        5、清空待处理订单

        

        6、一键重启处理失败的订单

        

        7、订单插队

        

      组件化封装

        完成了以上测试,基本上我想要的功能都已经实现了。但是仔细想了下,上述的功能里除了第一个下单接口是跟业务相关的,剩下的所有接口都是业务无关的。如果我们公司主营业务变了,从年夜饭变成中秋做月饼,端午包棕子,本服务中的大部分代码都可以在调整之后复用。那么,为什么我不整理出一个与业务无关的Redis队列工具出来呢,这样可以极大的提升代码的可复用性。后面有新的业务时,直接引入这个工具包,完善业务部分即可。

        以下是我在反思之后,对代码的整改(只包含有整改或新增的代码)

/*** 消息承载类** @author leixiyueqi* @since 2024/06/15 22:00*/
@Data
public class RedisQueueMsg<T> implements Serializable {/*** 消息Id*/private String id;/*** 服务名*/private String serverName;/*** 数据体*/private T data;
}package com.leixi.queue.pojo;import lombok.Data;import java.io.Serializable;
import java.util.Date;/*** 任务处理结果封装类** @author leixiyueqi* @since 2024/06/15 22:00*/
@Data
public class RedisResultVo implements Serializable {private String status;private Object data;private String desc;private Date startTime;private Date endTime;private String server;public RedisResultVo() {this.startTime = new Date();}public RedisResultVo(Object data) {this.data = data;this.startTime = new Date();}
}/*** 抽象的业务处理服务** @author leixiyueqi* @since 2024/06/15 22:00*/
public abstract class QueueBusiBasicService {/*** 处理任务的方法** @param obj 业务类*/public abstract void handle(Object obj);/*** 处理失败的回调方法** @param obj 业务类* @param e*/public abstract void callBack(Object obj, Exception e);
}/*** Redis队列的服务层** @author leixiyueqi* @since 2024/06/15 22:00*/
@Slf4j
@Service
public class QueueCommonService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Autowiredprivate Map<String, QueueBusiBasicService> serviceMap;@Value("${leixi.redis-queue-key}")private String REDIS_QUEUE_KEY ;/*** 插入消息到队列** @param obj 业务对象* @param serverName  服务名* @return*/public RedisQueueMsg sendMessage(Object obj, String serverName) {RedisQueueMsg msg = new RedisQueueMsg();msg.setId(IdUtil.fastSimpleUUID());msg.setServerName(serverName);msg.setData(obj);redisTemplate.delete(JSON.toJSONString(msg));redisTemplate.opsForZSet().add(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, JSON.toJSONString(msg), Constant.getScoreByDate());redisTemplate.convertAndSend(REDIS_QUEUE_KEY+ Constant.WORKING_QUEUE_SUFFIX, JSON.toJSONString(msg));return msg;}/*** 处理队列中的工作** @param message*/public void handle(String message) {Boolean flag = redisTemplate.opsForValue().setIfAbsent(message, "1", 1, TimeUnit.DAYS);// 加锁失败,已有消费端在此时对此消息进行处理,这里不再做处理if (!flag) {return;}redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, message);RedisQueueMsg param = CastUtils.cast(JSON.parseObject(message, RedisQueueMsg.class));RedisResultVo result = new RedisResultVo(param);try {result.setServer(InetAddress.getLocalHost().getHostAddress());serviceMap.get(param.getServerName()).handle(param.getData());result.setStatus(Constant.SUCCESS);}catch (Exception e) {e.printStackTrace();serviceMap.get(param.getServerName()).callBack(param.getData(), e);result.setStatus(Constant.FAIL);result.setDesc(e.getMessage());}finally {result.setEndTime(new Date());redisTemplate.opsForZSet().add(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, JSON.toJSONString(result), Constant.getScoreByDate());}}/*** 查询待处理任务** @return*/public Object getPendingTask() {return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);}/*** 清理待处理任务** @return*/public Object cleanPendingTask() {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);if(set.size() > 0) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY + Constant.PENDING_SUFFIX, set.toArray());}return "待处理任务已被清空!";}/*** 查询已处理任务** @return*/public Object getHandledTask() {return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, -1);}/*** 清理某天前的处理任务** @param day 天数* @return*/public Object cleanHandledTask(Integer day) {Set<String> set = redisTemplate.opsForZSet().rangeByScore(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, Constant.getScoreByDate() - day);set.forEach(s -> {RedisResultVo obj = JSONObject.parseObject(s, RedisResultVo.class);if (obj.getStatus().equals(Constant.SUCCESS)) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, s);}});return redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, System.currentTimeMillis());}/*** 重新处理已处理任务*/public String restartFailedTask() {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, 0, -1);StringBuilder sb = new StringBuilder();set.forEach(s -> {RedisResultVo obj = JSONObject.parseObject(s, RedisResultVo.class);if (!obj.getStatus().equals(Constant.SUCCESS)) {redisTemplate.opsForZSet().remove(REDIS_QUEUE_KEY+Constant.HANDLED_SUFFIX, s);RedisQueueMsg msg = JSON.parseObject(JSON.toJSONString(obj.getData()), RedisQueueMsg.class); ;sendMessage(msg.getData(), msg.getServerName());sb.append(msg.getId()).append(",");}});return "以下任务被重启: "+ sb;}/*** 任务插队** @param msgId 要插队的消息ID*/public RedisQueueMsg cutInLineTask(String msgId) {Set<String> set = redisTemplate.opsForZSet().range(REDIS_QUEUE_KEY+ Constant.PENDING_SUFFIX, 0, -1);for (String s : set) {RedisQueueMsg msg = JSONObject.parseObject(s, RedisQueueMsg.class);if (msg.getId().equals(msgId)) {CompletableFuture.runAsync(() -> {this.handle(s);});return msg;}}throw new RuntimeException("未找到相关任务,该项任务已经在执行了!");}
}/*** 业务服务类,继承抽象类务类,实现业务逻辑** @author leixiyueqi* @since 2024/06/15 22:00*/
@Service(Constant.DINNER_SERVER)
public class QueueDinnerService extends QueueBusiBasicService {@Autowiredprivate QueueCommonService queueCommonService;/*** 年夜饭下单** @param entity 订单信息* @return*/public Object orderNewYearEveDinner(OrderEntity entity) {// 存储订单信息saveOrder(entity);queueCommonService.sendMessage(entity, Constant.DINNER_SERVER);// 异步开始做菜return "您已成功下单,订单号为"+ entity.getOrderCode()+",后厨正在准备预制菜!";}/*** 这里模拟的是做年夜饭的过程方法,该方法用时较长,整个过程需要10秒,但是,这个过程中存在多种意外,该方法可能失败** @param req 订单信息*/private void doNewYearEveDinner(OrderEntity req) throws Exception {System.out.println("开始做订单 " + req.getOrderCode() + " 的年夜饭");Thread.sleep(10000);int i = new Random().nextInt(6) + 1;if (i ==4) {throw new Exception("厨师跑了");}if (i ==5) {throw new Exception("食物跑了");}if (i ==6) {throw new Exception("厨房着火了");}System.out.println("订单 " + req.getOrderCode() + " 的年夜饭已经完成");}/*** 保存订单信息** @param req*/private void saveOrder(OrderEntity req) {//这里假设做的是订单入库操作System.out.println("订单 " + req.getOrderCode() + " 已经入库, 做饭开始时间为 "+ new Date());}/*** 根据订单编号修改订单信息** @param orderCode 订单编号* @param dinnerStatus* @param remark*/private void updateOrder(String orderCode, String dinnerStatus, String remark) {// 根据订单编号修改订单的出餐结束时间,出餐状态,备注等信息。System.out.println("更新订单 "+ orderCode +" 信息,做饭结束时间为 "+ new Date() + ", 出餐状态为"+ dinnerStatus +", 备注为 " +remark);}/*** 处理订单** @param obj 业务类*/@Override@SneakyThrowspublic void handle(Object obj) {OrderEntity entity = JSON.parseObject(JSON.toJSONString(obj), OrderEntity.class);doNewYearEveDinner(entity);updateOrder(entity.getOrderCode(), Constant.SUCCESS, "出餐成功");}@Overridepublic void callBack(Object obj, Exception e) {OrderEntity entity = JSON.parseObject(JSON.toJSONString(obj), OrderEntity.class);System.out.println("更新订单 "+ entity.getOrderCode() +" 信息,做饭结束时间为 "+ new Date() + ", 出餐状态为FAIL, 原因为 " +e.getMessage());}
}@Component
public class RedisQueueListener implements MessageListener {@Autowiredprivate QueueCommonService service;private final Object lock = new Object();@Overridepublic void onMessage(Message message, byte[] pattern)  {synchronized (lock) {service.handle(message.toString());}}
}/*** 业务控制层** @author leixiyueqi* @since 2024/06/15 22:00*/
@RestController
@RequestMapping("/dinner")
public class DinnerController {@Autowiredprivate QueueDinnerService dinnerService;private int i = 0;@GetMapping("/orderDinner")public Object orderDinner() {OrderEntity entity = new OrderEntity();entity.setOrderCode("Order" + (++i));entity.setCustomerName("第"+i+"位客户");return dinnerService.orderNewYearEveDinner(entity);}}/*** Redis工具控制器** @author leixiyueqi* @since 2024/06/15 22:00*/
@RestController
@RequestMapping("/redisQueue")
public class RedisQueueController {@Autowiredprivate QueueCommonService service;@GetMapping("/getPendingTask")public Object getPendingTask() {return service.getPendingTask();}@GetMapping("/cleanPendingTask")public Object cleanPendingTask() {return service.cleanPendingTask();}@GetMapping("/getHandledTask")public Object getHandledTask() {return service.getHandledTask();}@GetMapping("/cleanOldSucceedTask")public Object cleanHandledTask(@RequestParam("day") Integer day) {return service.cleanHandledTask(day);}/*** 这里还有最后一个问题, 把已办里的错误的信息摘除出来,重新走请求。并且反馈哪些信息重新走了请求。*/@GetMapping("/restartFailedTask")public Object restartFailedTask() {service.restartFailedTask();return "重启失败的服务成功";}@GetMapping("/cutInLineTask")public Object cutInLineTask(@RequestParam("msgId")  String msgId) {RedisQueueMsg msg = service.cutInLineTask(msgId);return "任务 "+ JSON.toJSONString(msg) + "插队成功!";}}

        组件化的调整就是把属于Redis队列的操作与业务类操作完全分开,这样,以后有别的业务需要引入组件处理时,只需要写个业务服务继承QueueBusiBasicService即可,最大限度的复用了队列的这套机制和代码。

        注意,本组件有它特定的适用场景:处理任务的频度不高,每次处理任务用时较长,而且任务有一定的小概率失败,失败之后重新处理不会影响最终处理结果。

        完成这个工具研发后,我结合之前在网上查到的Redis队列的一些案例,发现用别的方案可以更简单的去实现我要的效果,比如直接用Redis队列详解(springboot实战)里的方案,仅仅是因为不想在代码里写 while(true) 这种不是优雅的代码,再加上用Listener的方式长时间没对消息进行消费时,消息会丢失,因此才额外花费了这么多的功夫来打补丁。果然方向选错了,工作量会成倍的增加,希望看到本文的读者能引以为戒,不要自误啊。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/34358.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何实现PHP开启OPcache?

一、OPcache是什么? 官方介绍:OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能, 存储预编译字节码的好处就是 省去了每次加载和解析 PHP 脚本的开销。 是不是有点看不明白? 给人一种朴实无华一点都不重点介绍的感觉? 其实说简单点就是这种缓…

靠AI一年增长15%,商场的春天来了么

文&#xff5c;艺 思 编&#xff5c;王一粟 “在商场全面部署AI机器人仅1年&#xff0c;AI带来的销售占同期整体联营销售额的比重逐步增加&#xff0c;最高达到了15%。” 这是正在银泰百货发生的真实案例。 除了线上电商&#xff0c;百货商场等线下的实体零售也正在尝试…

汇聚荣电商实力好不好?

在数字化浪潮的推动下&#xff0c;电商平台如雨后春笋般涌现&#xff0c;而“汇聚荣”作为其中的一员&#xff0c;其综合实力自然成为业界与消费者关注的焦点。那么&#xff0c;汇聚荣电商的实力究竟如何呢?接下来&#xff0c;我们将从多个维度深入探讨这一问题。 一、品牌影响…

创建和探索VGG16模型

PyTorch在torchvision库中提供了一组训练好的模型。这些模型大多数接受一个称为 pretrained 的参数&#xff0c;当这个参数为True 时&#xff0c;它会下载为ImageNet 分类问题调整好的权重。让我们看一下创建 VGG16模型的代码片段&#xff1a; from torchvision import models…

【JavaScript脚本宇宙】加速您的网站:图像优化工具和库的终极指南

别让大图拖垮你的应用&#xff1a;如何正确优化图像 前言 在数字时代&#xff0c;图像是我们日常生活中不可或缺的一部分。然而&#xff0c;随着图像数量的增加和分辨率的提高&#xff0c;它们也占据了越来越多的存储空间和带宽。为了解决这个问题&#xff0c;开发人员可以使…

什么美业系统好用?美业门店收银系统源码分享、小程序展示

专业美业系统与普通系统相比&#xff0c;更加贴合美业门店的经营需求&#xff0c;提供了更全面、便捷、高效的管理功能&#xff0c;有助于提升门店的服务质量和经营效益。 博弈美业系统包括PC、iPad、手机、小程序四大端口&#xff0c;满足不同人群的各种需求。客户可从小程序…

python并行批量存储mat文件

输入&#xff1a;包含数组的列表arrays_list&#xff0c;以及包含每个数组存储位置的列表save_path_list from concurrent.futures import ThreadPoolExecutor, as_completed from tqdm import tqdm from scipy.io import * def save_array_to_mat(array, filepath):savemat(f…

有什么能和ai聊天的软件?5个软件教你快速和ai进行聊天

有什么能和ai聊天的软件&#xff1f;5个软件教你快速和ai进行聊天 当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;技术已经逐渐渗透到我们的日常生活中&#xff0c;而与AI进行聊天也成为了一种趋势和乐趣。以下是五款可以和AI进行聊天的软件&#xff0c;它们提…

如何提高台式扫描电镜的放大倍数

台式扫描电镜&#xff08;SEM&#xff09;因其紧凑的设计和高效的成像能力&#xff0c;在材料科学、生物学和纳米技术等领域中扮演着重要角色。然而&#xff0c;用户在使用过程中可能会遇到需要更高放大倍数以获得更细微结构图像的情况。以下是一些提高台式扫描电镜放大倍数的策…

大厂面试官问我:Redis持久化RDB有没有可能阻塞?阻塞点在哪里?【后端八股文三:Redis持久化八股文合集】

往期内容&#xff1a; 大厂面试官问我&#xff1a;Redis处理点赞&#xff0c;如果瞬时涌入大量用户点赞&#xff08;千万级&#xff09;&#xff0c;应当如何进行处理&#xff1f;【后端八股文一&#xff1a;Redis点赞八股文合集】-CSDN博客 大厂面试官问我&#xff1a;布隆过滤…

与其他自动化配置管理工具(如 Ansible 、Chef )相比,Puppet 的独特优势和局限性分别是什么?

Puppet的独特优势包括&#xff1a; 基于声明式语言&#xff1a;Puppet使用自己的声明式语言&#xff08;Puppet DSL&#xff09;来描述系统配置&#xff0c;使得配置更加简洁、易于理解和维护。 完善的资源模型&#xff1a;Puppet具有丰富的资源模型&#xff0c;可以管理各种不…

C++ 入门

前言 c的发展史&#xff1a; C的起源可以追溯到1979年&#xff0c;当时Bjarne Stroustrup在贝尔实验室开始开发一种名为“C with Classes”的语言。以下是C发展的几个关键阶段&#xff1a; 1979年&#xff1a;Bjarne Stroustrup在贝尔实验室开始开发“C with Classes”。1983…

鸿蒙NEXT,保障亿万中国老百姓数据安全的操作系统

吉祥学安全知识星球&#x1f517;除了包含技术干货&#xff1a;Java代码审计、web安全、应急响应等&#xff0c;还包含了安全中常见的售前护网案例、售前方案、ppt等&#xff0c;同时也有面向学生的网络安全面试、护网面试等。 上周华为发布了最新的鸿蒙NEXT操作系统&#xff0…

windows系统上nginx搭建文件共享

1、下载windows版nginx 下载地址 2、配置nginx 编辑nginx.conf配置文件 在http模块下添加这个参数 underscores_in_headers on;#修改location内容&#xff0c;共享哪个文件夹&#xff0c;就写哪个文件夹&#xff0c;最后一定要跟上/&#xff0c;否则无法访问 location / {…

深入解析Ansible

文章目录 引言Ansible的原理Ansible的使用安装Ansible配置Ansible编写Playbook执行Playbook Ansible的优缺点Ansible的优点Ansible的缺点 总结 引言 在现代IT运维中&#xff0c;自动化工具扮演着至关重要的角色。Ansible作为一款开源的自动化运维工具&#xff0c;凭借其易用性…

Depth Anything环境搭建推理测试

引子 基于单目摄像头的深度估计&#xff0c;一直是CV领域的一个难点&#xff0c;之前也对此关注也不够多。偶然浏览技术博客&#xff0c;看到Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data这个最新CVPR2024的工作。看到名字&#xff0c;大概也能猜出来…

【机器学习300问】130、什么是Seq2Seq?又叫编码器(Encoder)和解码器(Decoder)。

Seq2Seq&#xff0c;全称为Sequence to Sequence&#xff0c;是一种用于处理序列数据的神经网络模型&#xff0c;特别适用于如机器翻译、语音识别、聊天机器人等需要将一个序列转换为另一个序列的任务。这种模型由两部分核心组件构成&#xff1a;编码器&#xff08;Encoder&…

服务器(Linux系统的使用)——自学习梳理

root表示用户名 后是机器的名字 ~表示文件夹&#xff0c;刚上来是默认的用户目录 ls -a 可以显示出隐藏的文件 蓝色的表示文件夹 白色的是文件 ll -a 查看详细信息 total表示所占磁盘总大小 一般以KB为单位 d开头表示文件夹 -代表文件 后面得三组rwx分别对应管理员用户-组…

shell的正则表达------awk

一、awk&#xff1a;按行取列 1.awk原理&#xff1a;根据指令信息&#xff0c;逐行的读取文本内容&#xff0c;然后按照条件进行格式化输出。 2.awk默认分隔符&#xff1a;空格、tab键&#xff0c;把多个空格自动压缩成一个。 3.awk的选项&#xff1a; awk ‘操作符 {动作}’…

pytorch库 03 基础知识

文章目录 一、准备工作二、tensorboard的使用1、add_scalar()方法2、add_image()方法 三、transforms的使用1、ToTensor()类2、常见transforms的类 三、torchvision中的数据集使用 官网 https://pytorch.org/ 一、准备工作 ①在pycharm和jupyter上&#xff0c;检查当前系统是…