注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

服务:简聊微内核结构

1 简介:微内核架构 微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。 微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。 该结...
继续阅读 »

1 简介:微内核架构


微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。


微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。


该结构是向最初并非设计为支持它的系统添加特定功能的最佳方式。


此体系结构消除了对应用程序可以具有的功能数量的限制。我们可以添加无限的插件(例如Chrome浏览器有数百个插件,称为扩展程序)


2 一个简单例子


微内核架构(也称为插件结构)通常用于实现可做为第三方产品下载的应用程序。此结构在内部业务程序很常见。


实际上,它可以被嵌入到其他模式中,例如分层体系中。典型的微内核架构有两个组件:核心系统和插件模块


	plug-in                  plug-in
core system
plug-in plug-in

漂亮一点的图


new_微内核架构.png


由上图可知,微内核架构也被称为插件架构模式(Plug-inArchitecture Patterm),通常由内核系统和插件组成的原因。


核心系统包括使系统正确运行的最小业务逻辑。可以通过连接插件组件添加更多功能,扩展软件功能。就像为汽车添加涡轮以提高动力。


轮圈37.png


插件组件可以使用开放服务网关计划(OSGi),消息传递,Web服务或对象实例化进行连接。
需要注意的是,插件组件是独立的组件,是为扩展或增强核心系统的功能,不应与其他组件形成依赖。


常见的系统结构使用微内核的如:嵌入式Linux、L4、WinCE。



  • 优缺点说明


微服务在应用程序和硬件的通信中,内核进程和内存管理的极小的服务,而客户端程序和运行在用户空间的服务通过消息的传递来建立通信,它们之间不会有直接的交互。


这样微内核中的执行速度相对就比较慢了,性能偏低这是微内核架构的一个缺点。


微内核系统结构相当清晰,有利于协作开发;微内核有良好的移植性,代码量非常少;微内核有相当好的伸缩性、扩展性。


3 小结


(1)微内核架构难以进行良好的整体化优化。

由于微内核系统的核心态只实现了最基本的
系统操作,这样内核以外的外部程序之间的独立运行使得系统难以进行良好的整体优化。


(2)微内核系统的进程间通信开销也较单一内核系统要大得多。

从整体上看,在当前硬件条件下,微内核在效率上的损失小于其在结构上获得的收益。


(3)通信损失率高。

微内核把系统分为各个小的功能块,从而降低了设计难度,系统的维护与修改也容易,但通信带来的效率损失是一个问题。


作者:楽码
来源:juejin.cn/post/7291468863396708413
收起阅读 »

如何在Java项目中实现漂亮的日志输出

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧...
继续阅读 »

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧和建议。



image.png



1. 使用合适的日志框架



  Java有许多优秀的日志框架可供选择,如Log4j、Logback和java.util.logging等。选择一个适合你项目需求的日志框架是实现漂亮日志输出的第一步。这些框架提供了丰富的配置选项,可以帮助你控制日志的格式和输出方式。这里对几个日志框架做一下简单的介绍。


Log4j


  Log4j是一个Java日志处理的框架,用于在Java应用程序中处理日志记录。它提供了一种灵活的方式来记录日志信息,并允许开发者根据需要配置日志输出的格式和目标。


  在Log4j中,主要有三个组件:Logger、Appender和Layout。Logger用于记录日志信息,Appender用于定义日志的输出目标,例如控制台、文件、数据库等,Layout用于定义日志的输出格式。


  以下是一个简单的Log4j代码示例:


import org.apache.log4j.Logger;  

public class MyApp {
// 获取Logger实例
final static Logger logger = Logger.getLogger(MyApp.class);

public static void main(String[] args) {
// 记录不同级别的日志信息
logger.debug("Debugging information");
logger.info("Informational message");
logger.warn("Warning");
logger.error("Error occurred");
logger.fatal("Fatal error occurred");
}
}

  在这个示例中,我们首先导入了Logger类,然后通过Logger.getLogger(MyApp.class)获取了一个Logger实例。在main方法中,我们使用Logger实例记录了不同级别的日志信息,包括Debug、Info、Warn、Error和Fatal。


Logback


  Logback是Log4j的改进版本,是SLF4J(Simple Logging Facade for Java)下的一种日志实现。与Log4j相比,Logback具有更高的性能和更灵活的配置。


  Logback的组件包括Logger、Appender、Encoder、Layout和Filter,其中Logger是最常用的组件。Logger分为rootLogger和nestedLogger,rootLogger是所有Logger的根,nestedLogger则是rootLogger的子级。Logger之间有五个级别,从高到低依次为ERROR、WARN、INFO、DEBUG和TRACE,级别越高,日志信息越重要。


  以下是一个简单的Logback代码示例:


import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

logging


  java.util.logging是Java平台的核心日志工具。


  java.util.logging由Logger、Handler、Filter、Formatter等类和接口组成。其中Logger是日志记录器,用于记录日志信息;Handler是处理器,用于处理日志信息;Filter是过滤器,用于过滤不需要记录的日志信息;Formatter是格式化器,用于格式化日志信息。


  这里介绍的日志框架,在项目当中运用的比较多的是Log4j、Logback,从基本的配置上没有太大的差异,大家也可以根据项目需求选择使用。



2. 定义清晰的日志级别



  在Java项目中,定义清晰的日志级别是非常重要的,以便在调试、监控和解决潜在问题时有效地记录和理解系统行为。下面是一些建议,可以帮助你定义清晰的日志级别:



  1. 了解常见的日志级别:Java中常见的日志级别包括DEBUG、INFO、WARN、ERROR和FATAL。每个级别都有特定的含义和用途,首先要了解这些级别的含义。

  2. 根据项目需求确定日志级别:在定义日志级别时,需要考虑项目的需求和目标。例如,对于一个简单的演示应用程序,可能不需要记录过多的调试信息。但对于一个复杂的业务系统,可能需要详细的调试信息来跟踪和解决潜在的问题。根据项目的重要性和规模来确定每个级别的日志信息是否必要。

  3. 默认级别设置:为项目设置一个默认的日志级别。这通常是INFO级别,用于记录系统的常规操作信息。

  4. 根据模块或功能设置日志级别:为每个模块或功能设置不同的日志级别。这有助于在特定部分出现问题时快速定位问题原因。例如,对于数据库模块,可以将其日志级别设置为DEBUG,以便记录详细的数据库操作信息。

  5. 日志级别继承:在一个日志级别下定义的日志信息,应该继承到其所有子级别中。这意味着,如果某个日志信息被设置为WARN级别,那么该信息应该同时出现在WARN、ERROR和FATAL日志中。

  6. 日志信息清晰明了:在记录日志信息时,要确保信息清晰明了,包含必要的细节。例如,对于错误信息,要包含错误类型、发生错误的方法和时间戳等信息。

  7. 日志轮转和清理:及时对日志进行轮转和清理,避免日志文件过大而影响系统性能。可以设置一个合适的大小限制或时间间隔,对旧的日志文件进行归档和清理。

  8. 培训开发人员:为开发人员提供关于如何使用日志系统的培训,确保他们了解如何记录适当的日志信息以及如何利用日志级别进行过滤。

  9. 参考最佳实践:可以参考一些关于日志编写的最佳实践指南,例如Log4j的官方文档,以获取更多关于如何定义清晰日志级别的建议。


  定义清晰的日志级别对于Java项目来说非常重要。通过了解常见的日志级别、根据项目需求确定级别、设置默认级别、按模块或功能划分级别、继承级别、记录清晰明了的日志信息、及时轮转和清理以及培训开发人员等措施,可以帮助你在项目中实现定义清晰、易于理解和使用的日志级别。



3. 格式化日志输出



  下面以Log4j为例,介绍如何格式化日志输出。


1,引入Log4j依赖


  在Maven项目中,可以在pom.xml文件中添加以下依赖:


<dependency>  
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.x.x</version>
</dependency>

2. 配置日志格式


  在log4j2.xml配置文件中,可以使用PatternLayout类来配置日志格式。例如,以下配置将日志输出为每行包含时间戳、日志级别、线程名和消息的格式:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  其中,%d表示时间戳,%t表示线程名,%-5level表示日志级别(使用五个字符的宽度),%logger{36}表示最长为36个字符的Logger名称,%msg表示消息。在配置文件中可以根据需要调整格式。


3. 在代码中使用Log4j记录日志


在Java代码中,可以使用以下语句记录日志:


import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;

public class MyClass {
private static final Logger logger = LogManager.getLogger(MyClass.class);

public static void main(String[] args) {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

  在输出结果中,可以看到每条日志信息都符合之前配置的格式。可以使用不同的配置文件来调整日志格式,以满足不同的需求。



4. 日志轮转和切割



  志切割和轮转在Log4j中主要通过两种策略实现:基于大小(Size-based)和基于日期时间(Time-based)。


1. 基于大小的日志切割和轮转


  这种策略是当日志文件达到指定大小时,会进行切割或轮转。例如,你可以设置当日志文件达到100MB时进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>

  在上述配置中,当app.log文件达到100MB时,它会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留20个这样的文件。


2. 基于日期时间的日志切割和轮转


  这种策略是当达到指定的日期时间时,进行日志切割或轮转。例如,你可以设置每天凌晨1点进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>

  在上述配置中,每天凌晨1点,app.log文件会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留30个这样的文件。


  注意:<DefaultRolloverStrategy max="20"/> 或 <TimeBasedTriggeringPolicy interval="1"/> 中的数字可以根据你的实际需要进行调整。



5. 日志过滤器(Filter)的使用



  Log4j中的过滤器(Filter)用于在日志事件发生之前对其进行一些条件判断,以决定是否接受该事件或者更改该事件。这可以让你根据特定的条件过滤日志输出,例如只打印错误级别以上的日志,或者根据线程ID、请求ID等过滤日志。


  在Log4j 2中,你可以通过配置文件(例如log4j2.xml)来为日志事件指定过滤器。以下是一个使用Log4j 2的XML配置文件中的过滤器的示例:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Filters>
<ThresholdFilter level="ERROR"/>
<MarkerFilter marker="FLOW" onMatch="DENY"/>
<MarkerFilter marker="EXCEPTION" onMatch="DENY"/>
</Filters>
</Console>
</Appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  在这个例子中,我们使用了三个过滤器:


  (1). ThresholdFilter只接受级别为ERROR或更高级别的日志事件。


  (2). 第一个MarkerFilter会拒绝任何带有"FLOW"标记的日志事件。


  (3). 第二个MarkerFilter会拒绝任何带有"EXCEPTION"标记的日志事件。


  另外,你还可以创建自定义的过滤器,只需实现org.apache.logging.log4j.core.filter.Filter接口即可。然后你可以在配置文件中通过指定类全名来使用你的过滤器。


  对于更复杂的过滤需求,你还可以使用Condition元素,它允许你使用Java代码来决定是否接受一个日志事件。不过,请注意,因为这可能会影响性能,所以应谨慎使用。


  下面是实际项目中打印的日志,大家可以根据项目的需求满足日志打印的需求。


image.png



总结



  通过选择合适的日志框架、定义清晰的日志级别、格式化日志输出、添加时间戳和线程信息、使用日志分级以及处理异常和堆栈跟踪,我们可以实现在Java项目中打印漂亮的日志。漂亮的日志输出不仅可以提高代码的可读性,还可以帮助我们更好地理解和跟踪代码的执行过程,从而提高开发效率和系统稳定性。


拓展阅读


# 35岁愿你我皆向阳而生


# 深入解读Docker的Union File System技术


# 说一说注解@Autowired @Resource @Reference使用场景


# 编写Dockerfile和构建自定义镜像的步骤与技巧


# 说一说Spring中的单例模式


# MySQL的EXPLAIN用法


# Spring的Transactional: 处理事务的强大工具


作者:mikezhu
来源:juejin.cn/post/7291675889381031990
收起阅读 »

HashMap扩容机制跟你的工作真的不相关吗?

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭 再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️ 那年 OK,书归正传,得益于本人工作态度良好...
继续阅读 »

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭



再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️


那年


OK,书归正传,得益于本人工作态度良好,同事和领导都给予了我很大的帮助,只记得那是18年的平常打工人的一天,我写了如下很多打工人都会写,甚至每天都在写的代码(当时的具体代码已经记不清了,现在大概模拟一下案发场景):


    /**
* 从Order对象中获取id属性并包装成List返回
*
* @param orderList Student列表
* @return idList
*/

public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>();
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


对没错,用Stream流可以一行代码解决这个问题,但当时受限于我们使用的JDK还是1.6和1.7,你懂得



我的直属领导看了我的代码后首先问我,你知道ArrayList初始化容量是多少吗?他是怎么扩容的?


我:。。。。。
img


这俩问题对现在的程序员来说兼职就是小菜一碟,不值一提,但对当时的我来说,可就有亿点难度了,之前面试之前依稀在那个博客上看别人写过,于是乎我就照着脑袋里模糊不清的知识点模棱两可的回答了这俩问题,emmm,


于是乎我领导就跟我说,既然你知道List容量不够会扩容,扩容会带来性能损耗(这个日后再细说,先说正事)那么你应该这么写,来避免它扩容呢?


    public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


千万不要小看这些细节哦



听君一席话,如听一席话,于是我悟了,


从那以后再有类似集合初始化的场景,明确知道容量的场景我都会初始化的时候传入构造参数,避免其扩容,无法知道确切容量的时候也会预估一下容量 尽可能的避免减少扩容次数。


去年


时间来到2022年,去年,我已经不是当年的那个懵懵懂懂愣头青了,坐我旁边的一个哥们(技术比我当年强多了去了),他写了一段初始化HashMap的代码也传入了一个初始容量,代码如下:


    public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}

img


敲黑板,重点来了,前面铺垫了那么多,就是为了说这事


历史惊奇在这一天重演,只不过负责问问题的是我


img


Q: 咳咳~HashMap的初始容量是16,放第几个个元素的时候会触发扩容呢(这题简单)


A: 元素个数超过16x0.75=12的时候进行扩容呗,扩容为16x2=32


Q: 既然容量为16,只能存12个元素,超过就会扩容,那么你写的new HashMap<>(orderList.size()) 这个能防止扩容吗?


A: emmm,不能


Q: 那初始化容量应该设置多少呢?


A: ……


Q: 16x0.75=12这个计算公式中, 初始容量变成未知假设为N 需存放的元素个数为20 Nx0.75=20N 是多少?(这大概就是经典的大学数学题吧)


A: 20➗0.75呗, 26.666 四舍五入27个, 设置容量为27,可以存放20个元素并且不触发扩容


img


所以正确的代码应该这么写: new HashMap<>((int) (orderList.size / 0.75 + 1))


别问为啥要+1,问就是因为小数转成int不会四舍五入直接舍弃小数点后的部分


一次轻松的对话就此结束


来看下大佬们是怎么写的


google的guava包 这是一个非常常用的java开发工具包,我从里面真的学到了很多(后续单独开篇文章记录一下)


//入口
HashMap<String, String> map= Maps.newHashMapWithExpectedSize(20);

public static <K extends @Nullable Object, V extends @Nullable Object>
HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap<>(capacity(expectedSize));
}

// 看这里看这里, 还考虑了一些其他的情况,专业!
static int capacity(int expectedSize) {
if (expectedSize < 3) {
checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
}
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
// This is the calculation used in JDK8 to resize when a putAll
// happens; it seems to be the most conservative calculation we
// can make. 0.75 is the default load factor.
return (int) ((float) expectedSize / 0.75F + 1.0F);
}
return Integer.MAX_VALUE; // any large value
}

大佬写的代码就是专业! img


org.apache.curator包 无意之间发现的,实现有点意思


//这里写法一样
HashMap<String, String> map = Maps.newHashMapWithExpectedSize(20);


public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap(capacity(expectedSize));
}

//看这里 看这里 expectedSize + expectedSize / 3
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? expectedSize + expectedSize / 3 : Integer.MAX_VALUE;
}
}

expectedSize + expectedSize / 3 说实话第一次看到这段代码的时候还是有点懵的,wocc这是啥写法,后来用几个数值带入计算了一下,还真是那么回事 👍🏻👍🏻


Hutool工具包


//入口
HashMap<String, String> map = MapUtil.newHashMap(20);

public static <K, V> HashMap<K, V> newHashMap(int size) {
return newHashMap(size, false);
}

//看这里, 平平无奇,什么档次?代码跟我写的一样,😄
public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
int initialCapacity = (int)((float)size / 0.75F) + 1;
return (HashMap)(isOrder ? new LinkedHashMap(initialCapacity) : new HashMap(initialCapacity));
}


说实话这个实现相较前者来说就显得不那么细了,居然跟我写的一样。。。


image-20231019160132153

这件事情带来的思考


说起HashMap的知识点,晚上的文章博客简直满天飞,大家现在谁还不能说上几句,但是! 后来在我面试的很多初中级开发时,我问他们准备往Map中存放20个元素,初始化容量设置多少不会触发扩容 时,基本上很少有人能答上来,10个人当中差不多有一个能回答上来?为什么会这样呢? 明明这些人是懂的初始容量16,超过出初始容量的75%会触发扩容,反过来问一下就不会了~😒 这充分说明了,学习要融会贯通举一反三,要细!!!


那段代码现在怎么写


据说JDK都出到21了,最近没怎么关注过~
不过JDK8已经流行很久了,那段代码用JDK8应该这么写:



  • list


public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}

//一行代码搞定,简洁明了
public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toList());
}

通过StreamCollectors.toList()来返回一个崭新的List,难道就没人好奇他这个List创建的时候有没有指定容量呢?如过不指定,在上面说到的那些明确知道存放容量的场景里岂不是要白白的扩容耗费性能???


答案是:NO 我们看下来Collectors.toList()的实现


public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

CollectorImplCollectors中的一个内部类,构造函数的第一个参数是Supplier<A> supplier这是一个函数式接口,就是说你得传给我一个实现,告诉我应该如何去创建一个集合,上面是这么传参的ArrayList::new, 这个写法其实就是new ArrayList(), 看到没!他并没有指定集合容量哦~~~


那么如果想提前指定好集合容量应该怎么写呢? 不卖关子了,直接贴代码了,写个B博客,真TM累死个人😌


public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toCollection(() -> new ArrayList<>(orderList.size())));
}

这就行了,看下Collectors.toCollection()的源码


public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}

这和Collectors.toList()基本上市一样的,只不过Collectors.toCollection()把如何创建集合的这个步骤抽象起来叫给我们开发者来个性化实现了,是不是又学到了一招~~~(#^.^#)



  • map


public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}


//这点破代码用Stream也是分分钟搞定
public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream().collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1));
}

和上面的List一样,这玩意初始化Map的时候也没有指定容量


public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

Map的创建也是通过一个函数式接口Supplier<M> mapSupplier定义的,传的参数是 HashMap::new,这也是一个方法引用,写法等于 new hashMap(), 想指定容量怎么办呢? 看代码


    public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream()
.collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1,
() -> new HashMap<>((int) (orderList.size() / 0.75 + 1))));
}

这个写法我们同样自己掌控如何创建需要的Map,容量自己定~


写在末尾


没啥要写的了,就这吧,累挺
img


作者:码塞顿开
来源:juejin.cn/post/7291828982558425142
收起阅读 »

01.你为什么需要学习K8S

前言 在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。 可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具Mesos和Docker Swarm,并将它们甩开了几十条街,成为了整个容...
继续阅读 »

前言



在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。


可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具MesosDocker Swarm,并将它们甩开了几十条街,成为了整个容器编排领域的龙头。



随着现在越来越多的企业把业务系统上云之后,大部分的服务都运行在Kubernetes环境中,可以说Kubernetes已经成为了云、运维和容器行业最炙手可热的工具,这也是为什么需要学习Kubernetes最重要的原因。


目前,AWS、Azure、Google、阿里云、腾讯云等主流公有云提供的是基于Kubernetes的容器服务。Rancher、CoreOS、IBM、Mirantis、Oracle、Red Hat、VMWare等无数厂商也在大力研发和推广基于Kubernetes的PaaS产品。


目前国内容器服务平台做的比较好的有腾讯云容器服务TKE阿里云容器服务ACK,它们都是基于K8S做的二开,有兴趣的读者可以自己了解和尝试使用。


K8S是什么?


K8S是单词Kubernetes的缩写,这个单词在古希腊语中是 [舵手] 的意思,之所以简称其为K8S,是因为'K'字母与'S'字母之间隔着八个单词,为了简便称呼,于是有了K8S这个简称。


K8S起初是Google内部的一个名为Borg的系统,据说Google有超过二十亿的容器运行在Borg上,在积累了十几年的经验之后,Google在2014年重写并开源了该项目,改名为Kubernetes


K8S在基于容器部署的方式上,提供了一个弹性分布式的框架,支持服务发现与负载均衡、存储、自动部署回滚、自动计算与调度、自动扩缩容等等一系列操作,目的是方便开发者不再需要关注服务运行细节,K8S能够自动进行容器与Pod调度、扩缩容、自动重建等等操作,保证服务尽可能健康的运行。


一句话来概括:K8S解放了开发者的双手,能够最大程度的让部署的服务健康运行,同时能够接入很多第三方工具(如服务监控、数据采集等等),满足开发者的定制化需求。


部署演变之路



传统部署时代


在互联网开发早期,开发者会在物理服务器上直接运行应用程序。以一个Go Web程序举例,很典型的一个部署方式是首先在本地编译好对应的二进制文件,之后上传到服务器,然后运行应用。


由于无法限制在物理服务器中运行的应用程序资源使用,因此会导致资源分配问题。例如,如果在同一台物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况,从而导致其他应用程序的性能下降。


虚拟化部署时代


为了解决上述问题,虚拟化技术被引入了。虚拟化技术允许你在单个物理服务器上运行多个虚拟机(VM)。虚拟化能够使应用程序在不同VM之间被彼此隔离,且能提高一定的安全性,因为一个应用程序的信息不能被另一应用程序随意访问。


虚拟化能够更好地利用物理服务器的资源,并且因为可以轻松地添加或者更新应用程序,而因此可以具有更高的扩缩容性,以及降低硬件成本等等的好处。通过虚拟化,可以将一组物力资源呈现为可丢弃的虚拟机集群。每个VM是一台完整的计算机,在虚拟化硬件之上运行所有的组件,包括自身的操作系统Guest OS


容器部署时代


容器类似于VM,但是具有更轻松的隔离特性,使得容器之间可以共享操作系统Host OS,并且容器不会像VM那样虚拟化硬件,例如打印机等等,只是提供一个服务的运行环境。



通常一台物理机只能运行十几或者数十个VM,但是可以启动成千上万的容器。因此,容器和VM比起来是更加轻量级的,且具有和VM一样的特性:每个容器都具有自己的文件系统、CPU、内存、进程空间等。


我们可以简单理解为:一个VM已经是一台完整的计算机了,而容器只是提供了一个服务能够运行的所有环境。


同时,因为容器与基础架构分离,因此可以跨云和OS发行版本进行移植。


容器部署具有以下优势



  • 敏捷部署:比起VM镜像,提高了容器镜像创建的简便性和效率。

  • DEVOPS:由于镜像的不可变性,可以通过快速简单的回滚,提供可靠并且频繁的容器镜像构建和部署。

  • 开发与运维的隔离:在构建、发布的时候创建应用程序容器镜像,而不是在部署的时候,从而将应用程序和基础架构分离。

  • 松耦合:符合微服务架构思想,应用程序被分解成一个个小服务运行在不同的容器中,可以动态部署和管理。

  • 软件/硬件层面隔离:通过namespace实现操作系统层面的隔离,如隔离不同容器之间的文件系统、进程系统等等;通过cgroup实现硬件层面的隔离,提供物理资源上的隔离,避免某些容器占用过多的物理资源CPU、Memory、IO影响到其他容器中的服务质量。


容器时代之后:Serveless


容器阶段之后,虚拟化仍然还在不断演化和衍生,产生了Serveless这个概念。


Serveless英文直译过来的意思是无服务器,这不代表着它真的不需要服务器,而是说服务器对用户不可见了,服务器的维护、管理、资源分配等操作由平台开发商自行维护。一个Serveless很经典的实现就是云函数,即最近火热的FAAS(Function As A Service),函数即服务。


Serveless并不是一个框架或者工具,它本质上是一种软件架构思想,即:用户无需关注应用服务运行的底层资源,比如CPU、Memory、IO的状况,只需要关注自身的业务开发。


Serveless具有以下特点



  • 无穷弹性计算能力:服务应该做到根据请求数量自动水平扩容实例,并且平台开发商应该提供无限的扩容能力。

  • 无需服务器:不需要申请和运维服务器。

  • 开箱即用:无需做任何适配,用户只需要关注自身业务开发,并且能够做到精确的按量计费。


强大的K8S


想像一个场景,假设我们现在把一个微服务架构的程序部署在成百上千个容器上,这些容器分部在不同的机器上,这个时候管理这些容器是一件非常让人头疼的事情。


让我们想想管理这些容器可能会碰到的问题,例如:



  1. 某个容器发生故障,这个时候我们是不是该启动另一个容器?

  2. 某台机器负载过高,那么我们之后的容器是不是不能部署在这台机器上?

  3. 某个服务请求量突增,我们是不是应该多部署几个运行该服务的容器?

  4. 如果某些容器之间需要相互配合怎么办?比如容器A需要容器B的资源,所以容器A一定要在容器B之后运行。

  5. 运行多个容器时,我怎么做到它们的运行结果是原子性的?即要么全部成功,或者全部失败。亦或者如果某一个容器失败,我能够不断重启这个容器以达到我的预期状态。


以上问题,都可以交给K8S来解决,它提供了一系列的功能来帮助我们轻松管理和编排容器,以达到我们的预期状态,同时因为它本身也是一个分布式高可用的组件,所以无需担心K8S出问题。


K8S官方文档这么描述它的功能:



  • 服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址来暴露容器。 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。

  • 存储编排 Kubernetes 允许你自动挂载你选择的存储系统,例如本地存储、公共云提供商等。

  • 自动部署和回滚 你可以使用 Kubernetes 描述已部署容器的所需状态, 它可以以受控的速率将实际状态更改为期望状态。 例如,你可以自动化 Kubernetes 来为你的部署创建新容器, 删除现有容器并将它们的所有资源用于新容器。

  • 自动完成装箱计算 你为 Kubernetes 提供许多节点组成的集群,在这个集群上运行容器化的任务。 你告诉 Kubernetes 每个容器需要多少 CPU 和内存 (RAM)。 Kubernetes 可以将这些容器按实际情况调度到你的节点上,以最佳方式利用你的资源。

  • 自我修复 Kubernetes 将重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器, 并且在准备好服务之前不将其通告给客户端。

  • 密钥与配置管理 Kubernetes 允许你存储和管理敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 你可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥


什么人需要学习K8S


运维/运开工程师


随着部署模式的演变,现在企业的应用几乎都以容器的方式在开发、测试、生产环境中运行。掌握基于K8S的容器编排工具的运维、开发能力将成为运维/运开工程师的核心竞争力。


软件开发人员


随着开发模式的演变,基于容器的微服务架构已经成为了开发应用首选的架构,而K8S是运行微服务应用的理想平台,市场会需要一批掌握K8S的软件开发人员。


GO开发人员


GO高级开发基本只有两个方向:高级服务端开发工程师和云原生工程师,其中云原生岗位会比高级服务端开发工程师更多。


这里的云原生主要是做DockerPrometheusKubernetes等云原生工具方向等等开发,这也是因为CNCF基金会的一系列产品基本都是使用Go语言写的,Go开发工程师相比于其他人员拥有天然优势。


总结


到这里,每天十分钟轻松入门K8S的01篇: 《你为什么需要学习K8S就结束了》 ,后续会持续更新相关文章,带大家了解K8S架构、K8S组件、如何搭建K8S集群、各种K8S对象、K8S高级特性、K8S-API等等内容。


欢迎大家点赞、收藏、催更~


作者:安妮的心动录
来源:juejin.cn/post/7291513540025434169
收起阅读 »

过度设计的架构师们,应该拿去祭天

我发现一个非常有趣的现象。 十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。 后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。 当然,现在互联网行业的架构师,也越来越...
继续阅读 »

我发现一个非常有趣的现象。


十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。


后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。


当然,现在互联网行业的架构师,也越来越“美女化”了,基本上有个两三年工作经验的,带两三个应届生负责过一两个QPS不过十,用户量不过千的小系统的,把项目用SSM框架给搭建起来的,也都成架构师了。



而这些所谓的“架构师”们,如果仅仅是title上的改动,平时工作中该撸代码就撸代码,该摸鱼看网页就看网页,其实也真的没什么。


最最最最怕的就是,他们觉得自己的身份已经变了,是时候该体现出自己作为系统架构师价值的时候了,那一切就会变得不可收拾了。


这些架构师们体现价值的方式当然是做架构设计。按照他们的话说,系统架构要具备前瞻性、灵活性、复用性、伸缩性、可维护性、可扩展性、低耦合性、高内聚性、可移植性。当然,基本上90%都是过度设计。



下面让我们来细数一下,那些年,我所经历过的过度设计。



名副其实的微服务


不久前我面试过一个中小厂架构师,看他的简历上赫然写着,“主导XX系统从单体服务往微服务架构演进工作”。


然后我问他的问题是:“详细说下微服务拆分这件事情,包括:微服务拆分的原因、时机和拆分后的粒度。”


这个架构师说的第一句话就把我雷到了:“微服务拆分的粒度,我认为越细越好,不然为什么叫微服务呢?而且,现在的一个很小的微服务,随着业务的持续迭代演进,未来都有可能变得非常庞大,我们做架构设计的,必须要具备前瞻性。”


他接着说:“我们的微服务不但按照业务模型进行的拆分,而且我还按照controller层、service层和dao层也做了拆分,这样可以提升代码复用性,你想用我哪层的代码,就可以调用我哪层的API。”


最终,一个单体服务就被他拆分成了这样。



我问他:“微服务的‘三个火枪手原则’了解吗?”


他摇了摇头,说不清楚。


我心里感慨到,今年阿里云和腾讯云业绩能不能达标,全看这类架构师的了,他们是真费机器啊。


3个库和300张表


去年,跟一个三方公司临时组建了一个项目组,共同开发孵化A项目。


项目联调期间,我跟三方公司的小A说:“我刚调用了你们项目的XX接口,新增了20条交易数据,你看看你们接口的业务处理正常吗?数据库里面有这20条数据吗?”


小A说:“好的,稍等,我看看。”


20分钟过去了,我问小A看得怎么样了。


小A说:“业务处理是正常的,数据我正在一条条找,20条已经找到17条了,我在找剩下的3条。”


我听得有些懵逼,问小A:”你直接从你们订单表里,不能一下子看到20分钟前写入的20条数据吗?为什么还需要一条条找啊?“


小A说:”我们的架构师老张,按照每天三百万订单的数据增量,做了一个五年架构规划,已经分好了3个库和300张表。我现在正在根据他的路由规则,一条条地找这些数据。“



满城尽是大中台


呵呵,忽如一夜春风来,满城尽是大中台。


2015年福厂正式提出了“大中台、小前台”的中台战略,通过将原本分散到各个业务的支持部门,比如技术部门、数据部门集中到一起,进行快速的服务迭代,以期更高效地支撑前线,大幅降低支持部门的重复投资建设。



三年后,各个大小互联网公司纷纷跟进,争相建设自己家的中台,也就在这时,某独角兽公司的架构师老范过来找我取经。


我跟老范说:“你们的两个主业务是机票和酒店,业务差别太大了,且创新孵化业务并不多,并不适合中台策略。”


老范说:“不,中台这个我们一定要搞,因为既是研发团队的政治任务,也是我个人的技术追求。”


半年后,我问老范搞得怎么样了,老范说:“唉,讨论了半年哪些职责属于大中台,哪些职责属于小前端,现在还没讨论明白呢。”


无处不在的消息队列


福厂收购了某公司,在收购后的一次技术交流中,我听到对方公司的首席架构师说:“MQ是个好东西,能异步处理,能消峰,能解耦,还是应该在项目中多用用的。”



后来发现,大首席架构师的下级执行力真强,MQ真的在他们的项目中无处不在:



  • 发短信验证码的场景用MQ,且其生产者和消费者是同一个服务,就为了用MQ异步调用短信服务的SDK;

  • 打业务日志的场景用MQ,且其生产者和消费者是同一个服务,就是为了用MQ异步打一行日志;

  • TPS个位数的约课场景用MQ,且其生产者和消费者是同一个服务,其美名曰进行消峰;

  • 各服务间的通信基本上80%都用了MQ,而不是RPC,其美名曰系统解耦;


牛逼Class!


遍地开花的多级缓存


对,对,还是上次的那个首席架构师,他除了爱用消息队列外,还特别喜欢用缓存,而且是Guava Cache + Redis的多级缓存。



据同事说,这种多级缓存策略在这位首席架构师的熏陶下,已经遍布了OA系统、公司官网、消息中心、结算系统、供应链系统、CRM系统。


首席架构师说:“缓存不仅能提升性能,还能帮助数据库抗压,提升系统可用性,绝对是个好东西,应该多用一用。”


然后,公司的系统就经常发生多种缓存的数据与数据库的数据一致性问题。


首席架构师又说:“任何架构都是有利有弊的,但只要利大于弊就好,不要太在意。”


设计模式的流毒


记得我刚上班不久,组内有一个架构师同事,写的代码巨复杂,各种技巧、设计模式、高级语法满天飞,还沾沾自喜的给我们炫耀。



一次Code Review的时候,我嘴欠问他这里咋这么设计,他就鄙视的说:“你连这个都不知道,这是设计模式中的建造者模式啊。”



当时觉得他好牛逼,而我好low。


以后,每次进行Code Review,只要看到其他同事代码里有几个if else,架构师同事就质问道:“为什么不用策略模式优化if else?”


当然,还有其他的质问,类似于:这块为什么不用抽象工厂模式?这块为什么不用代理模式?这块为什么不用观察者模式?


后来我们就给他起了个外号,叫“设模”(se mo)。


多多益善的复杂关系


前面说的那些架构师们,他们过度设计所带来的后果是浪费服务器和研发资源,但架构师老邓不一样,他的过度设计是浪费表。


之前见过某在线教育公司设计的表结构,基本上所有表之间的外键关系都是按照多对多方式设计的,也就是加一个中间的关系映射表。



有的我是可以理解的,比如:



  • 一个学生会出现在多个不同的班级里,而一个班级里也会有不同的学生;

  • 一个学生可以学习多门课程,而每门课程又会对多个学生进行学习;

  • 一个学生可以上多个老师的课,而一个老师又可以教多个学生;


但是,但是。



  • 一个学生可以有多个考试成绩,难道一个考试成绩还能属于多个学生吗?

  • 一个学生有多个课程的课时余额,难道一个课时余额还能属于多个学生吗?


老邓说:“万一以后业务变化了呢?一切皆有可能啊。”


数据库的可移植性


还在上大学的时候,在CSDN上看某著名架构师在极力强调数据库的可移植性。



我记得当时的原话大概是:



  • Hibernate的HQL可以帮我们保证不同数据库之间的移植性,比如:MySQL中的limit和Oracle中的rownum。

  • 为什么不能写存储过程?一个重要的原因就是业务逻辑放到数据库里会导致数据库移植成本变大。

  • 程序内尽量采用标准SQL语法,因为我们要考虑将来的移植风险。


当时听了,觉得这个大架构师简直就是YYDS。然后我工作了这么多年,也没遇到过一次数据库移植。


无间道版的数据校验


我厂某团队的架构师老李素以严谨著称,其经常放在嘴边的一句话就是:“工程师不仅仅是一项有创造性的职业,也是一门严谨审慎的职业。”


这话说的确实没毛病,我也看过他们团队的工程代码,程序的边界处理、异常处理和容错处理做得都特别好,入参校验也是特别细致入微。


就像老李所说的那样:“All input is evil。”


不,等等,入参校验没问题,但怎么从数据库里读出来的数据,为什么还要再校验一遍?难道不是在写入的时候校验吗?


老李面无表情地说:“如果数据库中的数据,并没有经过应用程序处理,而是不知道被谁直接改库了呢?”


卧槽,这是泥马数据校验无间道版吗?



疯魔成活的配置化


还是上面的那个架构师老李,他要求团队代码中带数字的地方,全部走配置中心,这样可以不发布代码就直接进行修改。



然后,我就看到了这样的现象:



  • 如果某个HashMap的size大于0,则进行xxxx,这个0写到了配置中心里。

  • 如果用户性别等于1(男性),则进行男装推荐,这个1写到了配置中心里。

  • 如果商品状态等于2(已下线),则进行xxxx,这个2写到了配置中心里。


配置中心啊,你的责任真的好重大。


总结


遇到这种类型的架构师,真的特别想把他们祭天了,因为我是Kiss原则的忠实拥趸。



Keep it simple,stupid,即:保持简单、愚蠢。


保持简单就能让系统运行更好,更容易维护扩展,越是资深的人,越明白这个道理。


作者:库森学长
来源:juejin.cn/post/7287144182967107638
收起阅读 »

看了我项目中购物车、订单、支付一整套设计,同事也开始悄悄模仿了...

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助...
继续阅读 »

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助!



mall项目简介


这里还是简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 的电商系统(Github标星60K),采用Docker容器化部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能,功能很强大!



后台管理系统演示



前台商城项目演示



功能设计



这里介绍下从商品加入购物车到订单支付成功的整个流程,涵盖流程的示意图和效果图。



流程示意图


以下是从商品加入购物车到订单支付成功的流程图。



流程效果图


以下是从商品加入购物车到订单支付成功的效果图,可以对照上面的流程示意图查看。



数据库设计


为了支持以上购物流程,整个订单模块的数据库设计如下。



设计要点



接下来介绍下整个购物流程中的一些设计要点,涵盖加入购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容。



加入购物车


功能逻辑


用户将商品加入购物车后,可以在购物车中查看到商品。购物车的主要功能就是存储用户选择的商品信息及计算购物车中商品的优惠。



购物车优惠计算流程



相关注意点



  • 购物车中商品优惠金额不会在购物车中体现,要在生成确认单时才会体现;

  • 由于商品优惠都是以商品为单位来设计的,并不是以sku为单位设计的,所以必须以商品为单位来计算商品优惠;

  • 代码实现逻辑可以参考mall项目中OmsPromotionServiceImpl类的calcCartPromotion方法。


生成确认单


功能逻辑


用户在购物车页面点击去结算后进入生成确认单页面。确认单主要用于用户确认下单的商品信息、优惠信息、价格信息,以及选择收货地址、选择优惠券和使用积分。



生成确认单流程



相关注意点



  • 总金额的计算:购物车中所有商品的总价;

  • 活动优惠的计算:购物车中所有商品的优惠金额累加;

  • 应付金额的计算:应付金额=总金额-活动优惠;

  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateConfirmOrder方法。


生成订单


功能逻辑


用户在生成确认单页面点击提交订单后生成订单,可以从订单详情页查看。生成订单操作主要对购物车中信息进行处理,综合下单用户的信息来生成订单。



下单流程



相关注意点




  • 库存的锁定:库存从获取购物车优惠信息时就已经从pms_sku_stock表中查询出来了,lock_stock字段表示锁定库存的数量,会员看到的商品数量为真实库存减去锁定库存;




  • 优惠券分解金额的处理:对全场通用、指定分类、指定商品的优惠券分别进行分解金额的计算:



    • 全场通用:购物车中所有下单商品进行均摊;

    • 指定分类:购物车中对应分类的商品进行均摊;

    • 指定商品:购物车中包含的指定商品进行均摊。




  • 订单中每个商品的实际支付金额计算:原价-促销优惠-优惠券抵扣-积分抵扣,促销优惠就是购物车计算优惠流程中计算出来的优惠金额;




  • 订单号的生成:使用Redis来生成,生成规则:8位日期+2位平台号码+2位支付方式+6位以上自增id;




  • 优惠券使用完成后需要修改优惠券的使用状态;




  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateOrder方法。




取消订单


功能逻辑


订单生成之后还需开启一个延时任务来取消超时的订单,用户也可以在订单未支付的情况下直接取消订单。



订单取消流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的cancelOrder方法。


支付成功回调


功能逻辑


前台用户订单支付完成后,第三方支付平台需要回调支付成功接口。



支付成功回调流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的paySuccess方法。


总结


今天给大家介绍了mall项目中整套购物流程的功能设计,其实对于很多网站来说都需要这么一套功能,说它是通用功能也不为过。从本文中大家可以看到,mall项目的整套购物流程设计的还是比较严谨的,考虑到了方方面面,如果你对mall项目整套购物流程实现感兴趣的话可以学习下mall项目的代码。


项目源码地址


github.com/macrozheng/…


作者:MacroZheng
来源:juejin.cn/post/7290931758787313725
收起阅读 »

2023年震撼!Java地位摇摇欲坠?Java在TIOBE排行榜滑坡至历史最低!

一、Java掉到历史最低 从2023年6月开始Java掉到历史最低排到第4位 2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。 top 10 编程语言1988年~2023年历史排名 引用...
继续阅读 »

一、Java掉到历史最低


从2023年6月开始Java掉到历史最低排到第4位



2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。



top 10 编程语言1988年~2023年历史排名



引用tiobe官网上TIOBE Software 首席执行官的话:


10 月头条:C# 越来越接近 Java


C# 和 Java 之间的差距从未如此之小。目前,差距仅为 1.2%,如果保持这种趋势,C# 将在大约 2 个月的时间内超越 Java。在所有编程语言中,Java 的跌幅最大,为 -3.92%,C# 的涨幅最大,为 +3.29%(每年)。这两种语言一直在相似的领域中使用,因此二十多年来一直是竞争对手。Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。过去,C#只能作为商业工具Visual Studio的一部分。如今,C# 是免费且开源的,受到许多开发人员的欢迎。Java 的衰落还有其他原因。首先,Java 语言的定义在过去几年中没有发生太大变化,而其完全兼容的直接竞争对手 Kotlin 更易于使用且免费。——TIOBE Software 首席执行官 Paul Jansen


二、编程语言排行榜


编程语言排行榜是一种用来衡量编程语言的流行度或受欢迎程度的指标,它通常会根据一些数据或标准来对编程语言进行排序和评价。不同的编程语言排行榜可能会有不同的数据来源、计算方法和评估标准,因此它们的结果也可能会有所差异。


目前,最知名和权威的编程语言排行榜之一是 TIOBE 编程社区指数,它由成立于 2000 年 10 月位于荷兰埃因霍温的 TIOBE Software BV 公司创建和维护。TIOBE 编程社区指数通过对网络搜索引擎中涉及编程语言的查询结果数量进行计算,来衡量各种编程语言的受欢迎程度。TIOBE 编程社区指数每个月都会更新一次,并且每年还会评选出一门年度编程语言,表示该门语言在当年的排名中上升幅度最大。除了 TIOBE 编程社区指数之外,还有一些其他的编程语言排行榜,以下是列举的一些编程语言排行榜。


1、TIOBE编程语言排行榜


TIOBE是一家荷兰的编程软件质量评估公司,每月发布一份编程语言排行榜。它使用搜索引擎查询结果、开发者社区活跃度和其他指标来评估编程语言的受欢迎程度。



2023年10月TIOBE编程语言排行榜



2、Stack Overflow开发者调查


Stack Overflow每年进行一次开发者调查,其中包括有关最受欢迎编程语言的信息。Stack Overflow 开发者调查是最权威的编程语言排行榜之一,该调查可以反映全球开发者对编程语言的喜好和使用情况。在选择编程语言时,可以参考该调查的结果,但也需要根据自己的实际需求和开发环境进行综合考虑。



连续三年最受欢迎编程语言排名,可以明显的看出Java的占比在逐年的降低



3、GitHub编程语言趋势榜


GitHub提供了一个编程语言趋势页面,显示了开发者在GitHub上使用的编程语言趋势。虽然这不是正式的排行榜,但反映了实际的开发趋势。


GitHub在趋势榜比较前的基本者是Python或Go的项目



GitHub官网已经去掉了top的排名榜只保留了趋势榜,由一些GitHub的爱好者和贡献者创建和维护的www.github-zh.com的GitHub中文社区网站,是非官方github网站,它旨在为中文用户提供GitHub的相关资讯、教程、交流和协作平台,还可以查到Github项目排行榜。



三、展望Java


可以看到各种编程语言排行榜的数据,虽然会存在片面的情况,但也大体能表现出Java的地位在下降,遥想当年Java是排行榜霸榜老大哥。



虽然Java明显下降,或许正如TIOBE首席执行官说的“Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。”在这个开放的世界里真正的开源而不是利用开源来测试付费项目才能真正的让大家推崇。


Java的许可模式变化导致用户流失。自从Java 8之后,甲骨文公司决定对Java的商业使用收取费用,这使得一些企业和开发者转向其他免费或开源的语言,如C#、Python等 。


Java的竞争对手不断发展和创新,提供了更多的选择和优势。例如,C#在.NET平台上不断完善和扩展,支持跨平台、混合开发、WebAssembly等技术 ;Python在数据科学、人工智能、Web开发等领域有着广泛的应用和生态 ;Kotlin作为Android官方推荐的语言,兼容Java,并提供了更多的语法糖和功能 。




Java虽然在编程语言排行榜上有所下降,但并不意味着Java就没有前途和价值。Java仍然是一门成熟、稳定、高效、跨平台的语言,拥有庞大的用户群和丰富的生态系统。Oracle作为Mysql、Java等重量级项目的拥有者,也在不断地改进和创新Java,让Java能够适应时代的变化和需求。包括Java 17的免费、Kafka/Spring Boot新版本最低的Java版本为17、Java 21引入协程等,都是Oracle在努力让Java保持竞争力和活力的例证 。



未来在不断的变化,说不定马斯克的美女机器人就真的造出来了。。。


当然,我们也不能忽视其他编程语言的发展和优势,我们应该保持开放和学习的心态,了解不同语言的特点和适用场景,选择最合适的语言来解决问题。编程语言只是工具,重要的是我们能够用它们创造出有价值的产品和服务。


作者:玄明Hanko
来源:juejin.cn/post/7290849115721285667
收起阅读 »

可别小看了一边写代码嘴里一边叨咕的同事,人家可能用的是小黄鸭调试法

什么,鸭子还能调试代码?什么神奇的鸭子啊。 当然不是了,是鸭子帮你调试,那好像也有点儿厉害。 初听感觉是傻子,再听感觉是玄学。 什么是小黄鸭调试法 当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。 小黄鸭调试法(Rubber Duck Deb...
继续阅读 »

什么,鸭子还能调试代码?什么神奇的鸭子啊。


当然不是了,是鸭子帮你调试,那好像也有点儿厉害。


初听感觉是傻子,再听感觉是玄学。



什么是小黄鸭调试法


当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。


小黄鸭调试法(Rubber Duck Debugging)是一种常用于解决编程问题的技巧,不是代码技术层面的技巧。


大致的调试过程是这样的:



  1. 首先你写好了代码,或者有些逻辑一直写不出来,然后很有自信或者不自信;

  2. 然后你找到一只鸭子,玩具鸭子,或者任意一个电脑旁边的物件;

  3. 最后,把你的代码的逻辑尽量详细的讲个上一步找到的对象,比如一只玩具鸭子;

  4. 通过讲解的过程,你很有可能发现代码上的漏洞,有时候还能发现隐藏的漏洞;



你还可以拉过旁边的人,对着他讲,前提是保证别人不会打你。


这个过程更像是一种review的过程,而且是那种非常具体的review,只不过是自己 review 自己的代码或逻辑。


它的核心是通过将问题或逻辑用语言描述出来,在这个过程中找到解决问题的线索。


虽然这个方法听起来可能有点奇怪,但它在实际中确实能够帮助很多人解决问题。解释问题的过程可能会强迫你慢下来,更仔细地思考,从而找到之前忽略的问题点。


另外,在进行这一些列操作的过程中,尽量保证周围没有人,不然别人可能觉得你是个傻子。


当然了,这个操作你可以在心里默默进行,也是一样的效果。


各位平时工作中有没有遇见过有人使用小黄鸭调试法呢?我看到这个概念的时候想了一下,好像还真碰到过。之前有同事在那儿写代码,一边写嘴里一边叨咕,也不知道在说啥,还开玩笑说这是不是你们这个星座的特质(某个星座)。


现在想想,人家当时用的是不是小黄鸭调试法呀,只恨当初孤陋寡闻,没有问清楚啊。


内在原理


小黄鸭调试法的内在原理其实是涉及到认知心理学中的一些概念的,并不真的是玄学和沙雕行为。


认知外部化


这是小黄鸭调试法的核心。当你将问题从内心中的思考状态转移到外部表达时,你会更加仔细地思考问题。解释问题需要你将问题的细节和步骤以清晰的语言描述出来,这个过程可以帮助你整理思路,更好地理解问题。


问题表达


描述问题的过程可以迫使你更具体地考虑问题。将问题分解为不同的部分,逐步地解释代码的执行流程,有助于你更好地理解代码中可能的缺陷或错误。


观察问题:


当你通过语言表达问题时,可能会注意到之前忽略的细节。这可能是因为你在描述问题时需要更仔细地审查代码和逻辑,从而让你注意到潜在的问题点。


听觉和口头处理


讲述问题的过程涉及到将问题从书面表达转化为口头表达。听觉和口头处理可以帮助你以不同的方式来理解问题,可能会在你的大脑中触发新的洞察力。


认知切换:


与代码一起工作时,你可能会一直陷入相同的思维模式中,难以看到问题。通过将问题从代码中抽离出来,并通过描述来关注它,你会进行认知切换,从而能够以不同的角度审视问题。


总结起来其实很简单,如果一个知识点你理解了,你一定能给别人讲出来,或者写出来,而且别人能够理解。如果你在讲的时候发现有模棱两可的地方,那说明你还没有百分百理解。


就像我们平时写技术文章一样,有时候碰到一些细节写半天也写不清楚,那就是还没有完全理解。


作者:古时的风筝
来源:juejin.cn/post/7290932061174022159
收起阅读 »

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟云端源想官网传送门⭐📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈🎯章节实...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆


729cc0d1da3852fe1d8a0eb81a6b357b.png


🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡

ddcdaae2cab0bac546d4d195018866f0.png


🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍


910fa68dda93dd2594530d0e5af268c7.pngd419dacb74e778686c40897862075ac8.png

收起阅读 »

谈谈成长

背景 距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了 一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反 馈非常好, 老板看了将录屏在整个研发部进行公布...
继续阅读 »

背景


距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了
一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反
馈非常好, 老板看了将录屏在整个研发部进行公布并且推荐大家观看, 这是部门内分享活动以来第一次公示与推荐录
屏的情况, 我自己也感觉非常感慨, 毕业以来, 一直朝着“一年入门, 三年高工、五年资深、七年架构”的目
标前进, 有时候回过头来会发现这一路还是挺有意思的, 借着这样一个平台, 以文字的形式描述出来, 期望
能够给前行路上的各位有一定的帮助


下面的描述中, 为了不暴露个人信息, 对公司名称的描述统一替换为 XX, 涉及到人名的我都会进行打码


01.png


一、为什么做这个分享(why)


1.1、原因一


主管说从实习加入XX三年半以来, 看到我成长了很多, 推荐我可以做一下关于个人成长这一块的分享


1.2、原因二


我个人仔细的想了一下, 三年半前加入XX, 当时公司才400多人, 没有一个正式的java团队(公司主c#), 三年半的时间, 公司发展到了750多人, 我经历了java团队从0到1的完整过程(目前已经有3个java团队, 加起来有接近30个java开发), 并且在这三年半的时间, 公司的后端业务项目开始从.net往java迁移, 有大量的重构项目和新的大型项目, 在此期间, 经历了可能有10个以上从0到1的大型项目的落地, 并且这些项目中我个人承担了绝大部分的核心功能开发, 在这些项目的锻炼下, 不管是技术还是工作方式方面, 都有学习到非常多的知识, 借着这样的机会复盘一下, 继续完善自己, 我主观认为这样的经历可能会有一定的参考价值, 期望能够通过这样的一个分享, 能够给一些可能遇到瓶颈或者跟我遇到一样问题的同学一些启发


二、工作方式上的成长及建议


2.1、需求方案的决策


Pre: 遇到问题 / 需求我应该怎么做, 还没有独当一面的能力


Now: 遇到问题 / 需求我思考可以怎么做, 并从自己的角度出发提出方案, 标识每个方案的优缺点, 询问应该怎么做(即使提出的方案都不是最优解, 但是有自己的思考, 并且在遇到更合适的方案时能有一个对比学习)


截图是两个例子, 在我实现功能的时候, 有不同的方案, 在把握不准哪种方案比较合适的情况下, 向主管进行询问, 对可行的方案的优缺点进行分析, 在主管多年的经验下确定最终的实施方案


02.png


03.png


2.2、需求功能的实现和上线


Pre: 需求处理完, 简单的自测或者压根就不自测, 测试流程走完以后发上线就不管了(在工作过程中其实有遇到许多同事也是这样), 在刚来实习以及刚毕业那段时间, 一个功能编码完以后测试反馈了许多的bug问题, 改了一个旧的出现一个新的, 之前有听过一个段子, 测试听到开发说的最多的是什么(我没改代码 / 网络问题 / 你再试试), 这个段子就在我身上出现了, 当时改一个功能, 连续几次都没改好(因为改完后没有充分验证就提测了), 见下图(2021年的聊天记录)


Now: 需求处理完, 充分的自测, 提测以后多次跟进测试同学的测试情况, 上线后跟进线上是否正常使用, 找产品进行验收流程


Suggest: owner意识、需求从哪里开始就从哪里结束, 回调产生事件的人, 形成闭环


owner意识是主管在小组中提出来的一个工作方式, 是说一个开发, 在多人协作的场景下, 不能只关注自己那一块的功能, 需要对整个流程有充分的了解, 以一个owner的角色参与到项目开发中, 跟进其他端的对接, 把控整个项目的进度, 要做到这个其实是很难的, 需要花费更多的精力, 但是一旦做到了, 对整个项目就有一种非常清晰的感觉, 能够更好的完成多人协作项目的落地, 需求从哪里开始就从哪里结束, 每次一个需求处理完以后, 我会严格按照 自测 -> 提测 -> 联调 -> 督促产品验收 -> 灰度环境发布及验收 -> 正式环境发布及验收 -> 同步所有有关开发自己的功能的发布情况


在这样一个闭环的链路中, 功能的落地和稳定有了非常明显的提高


04.png


2.3、句句有回应, 事事有着落


Pre: 忙的时候忘记回消息, 事情多的时候忘记一些临时分配的事情, 遇到的坑重复跳


Now: todoList、QQ置顶, 多检查(防止多次返工), 防止重复的问题重复出现(错别字)


todoList:
我是通过一个txt文件来进行一个记录的(当然有更合适的, 只是用习惯了), 会记录手上的需求有哪些, 每一个的进度是怎么样的, 上线流程是怎么样的, 我给自己定的上线流程中分为以下几步, 并且在上线的过程中严格的按照下面的流程进行操作, 这样极大的提高了上线的稳定性(特别是一些大版本的发布, 有这样的严格流程下, 稳定性有了非常明显的提高)


1、是否有旧数据需要处理


2、是否有sql脚本需要执行


3、是否依赖于其他人的功能


4、是否有单独服务器灰度的需求, 有些业务需要单独一台服务器来进行灰度, 然后通过nginx将部分流量转发过来验收


5、开始灰度, 灰度后产品验收, 全部负载发布完毕后回调需求开始的人, 形成闭环


QQ置顶: 我们公司是采用QQ作为沟通工具的(可能是因为很早之前就是用这个, 即使现在有企业微信, 但是沟通这一块大家都不太倾向于切到企业微信), 当在外面或者遇到其他比较紧急事情时有人找或者发消息, 可能看完就忘记回了, 于是我养成一个习惯, 有人发消息没时间回的, 第一时间置顶, 等有空后再来跟进, 跟进完后取消置顶


防止重复的问题重复出现: 我们需要写周报, 刚开始的时候有出现错别字、格式不正确、周报字数过多的情况(主管规定周报不能超过250个字), 后面为了避免这种情况, 我每次写完都会阅读两遍内容, 防止之前产生的问题重复发生, 每个人都有粗心的时候, 也不能保证不出现错误, 但是我们可以通过一些方式方法来尽量的避免错误的重复发生


2.4、学习别人优秀的地方


人无完人, 比如我有时候说话会比较直接(好吧, 我是直男....), 那时主管就让我在沟通这一块多跟斜对面的同事学习一下, 后面我就观察他的沟通方式, 然后自己从模仿开始, 慢慢的也学会了怎么进行有效的沟通、委婉的拒绝等, 学习别人优秀的地方也是成长中比较重要的一环


2.5、批量接口的重要性


Pre: 对需求的时候遇到一个问题就问一次, 效率低, 刚来实习的时候, 不熟悉业务, 当时接触到一块比较复杂的涉及到购物车优惠计算的业务, 当时跟对方讨论的时候, 遇到一个不懂的就直接去问, 多次打断他的工作, 效率非常低


Now: 对整个需求进行梳理, 整理所有的疑问一次性请教, 效率提高了很多, 也不会容易打断别人的工作(反到现在我经常被别人打断=,=看到了曾经的我....)


2.6、做的事太简单, 没有挑战性


今年带了一个实习生, 刚开始给他分配的工作都是比较简单的, 主要是修bug、小需求, 但是即使是这样简单的功能, 他做的也是磕磕碰碰, 比如代码规范不达标、空指针异常判断不全面、业务涉及到的点想的不全面等等许多瑕疵, 多次需要我来进行一个兜底, 后面他跟HR反馈说太简单没有挑战性, 于是我就分配了一个稍微大的一点需求给他, 结果做的惨不忍睹, 后面他实习结束时这个需求才做到一半, 我转给其他同事帮忙接手....结果那个同事看到代码后对我吐槽了许久....


当时收到反馈后, 我及时的找他进行沟通, 我的观点是他做的每一个简单的需求, 需求的完整稳定上线都是为了给主管建立一个做事靠谱的印象, 如果做事不靠谱, 经常需要他人来进行兜底, 那么谁也不敢把复杂重要的任务交给你做, 公司很多业务都涉及到商家的钱, 一旦这种重要的业务出问题, 那么会给公司造成巨大损失, 其实仔细想想, 自己实习那会也差不多, 想要做很厉害的项目, 用很流行的技术, 但是如果自己给人的感觉是不靠谱的, 那么主管自然就不敢把这些项目交给我了, 明白了这一点后, 当时我毕业一年给主管的目标是一个需求功能下来, 不管大小, 测试反馈的bug不能超过3个, 一个月产生的线上事故不能超过1次, 我努力做到了这一点, 线上事故几乎没发生过, 随之而来的是小组中绝大部分核心的业务、基础组件的开发都由我来处理, 以及一波大的涨薪


2.7、内卷


维基百科: 原是一个社会学概念,指一种文化模式发展到一定水平后,无法突破自身,只能在内部继续发展、复杂化的过程。大约从2018年开始,“内卷”一词在中国大陆变得广为人知,并引申表示付出大量努力却得不到等价的回报,必须在竞争中超过他人的社会文化,包含了恶性竞争、逐底竞争等更为负面的含义。


我们公司965几乎不加班, 所以我们6点会有比较充足的时间, 我一般去楼下吃完饭以后就会回到公司学习, 三年半的时间阅读了大量的源码书籍、学习了许多知识, 个人认为, 深入学习技术, 提高自己其实不属于内卷, 无意义的加班, 内耗等才是内卷, 其他小组也有人说我比较卷, 但是我们小组(包括主管)都是知道我晚上下班后是在公司学习的, 也很少会加班, 借着分享的机会我也跟大家澄清了这样的情况, 并且我们小组的氛围并没有因为我下班后的学习而导致整个小组都晚下班的情况(公司7点左右就基本空了.....)


三、技术能力上的成长及建议


3.1、如何学习


一、我学习一门未接触过的技术时, 会先看视频学习, 建立基本认识, 并且能够从讲师身上学到一些经验, 即先学会简单的使用
二、在了解了基本使用, 并且用起来的情况下, 我会查找跟该技术有关的权威书籍, 对权威书籍的学习是为了建立完整的知识体系


这个学习方式是我从大学以来就保持的, 而我认为这也是对我来说是最合适的学习方式


3.2、打地基-基础知识的重要性


基础知识对一个程序员来说是非常重要的, 个人认为基础越扎实的同学往往在学习技术的时候会吸收的更快, 并且也能够走的更远


一、数据结构, 我学习数据结构的时候, 会手写每个数据结构, 即使是最难的红黑树, 我也手写出来了(当时在大学的时候花了一个下午就为了写一个新增节点和删除节点的方法), 对于数据结构的学习, 我推荐: 恋上数据结构 这套视频, 讲的非常好, 大家如果有兴趣的话可以各显神通的去找找=,=


二、计算机网络, 计算机网络我是通过看视频加书籍的方式来学习的, 视频推荐: 韩立刚, B站就能搜到, 讲的通俗易懂, 我推荐了几个朋友看, 都反馈非常棒, 书籍推荐 计算机网络 第六版(考研408专用)


三、操作系统, 对操作系统的学习, 能够让我们在了解JVM、以及一些底层知识的时候(比如CAS、volatile、synchronize等原理)能够更加的顺利, 他们都是依赖于操作系统相关的知识来的, 视频我推荐: 哈工大的计算机操作系统, B站能搜到, 书籍推荐计算机操作系统(考研408专用)


四、汇编语言, 如果有看过深入理解Java虚拟机这本书, 那么里面就有出现跟汇编相关的话术, 如果对汇编有所了解, 能够亲身的体验到寄存器操作、中断的原理等, 这些在学习操作系统等知识的时候必然会遇到的话术, 视频我推荐推荐: 小甲鱼, B站就能搜到, 书籍我推荐(汇编语言(第3版) 王爽)


五、设计模式, 刚开始写代码的时候, 会一个方法写很多逻辑, 就像流水账一样, 一直写下去, 没有考虑复用等情况, 通过学习设计模式, 我们可以写出更加优雅的代码, 模板方法、单例、工厂等模式的使用能够使得我们的代码阅读性更高、扩展性更强, 学会了设计模式的情况下, 再去看自己之前写的代码就会发现还能写的更好! 并且有了这个知识的基础上, 我们去看一些框架源码的时候会更加顺利, 框架源码用到设计模式的时候命名都是通俗易懂的, 看到名字就知道用了什么模式, 就像程序员之前互相沟通一样, 这个我没有看视频, 我看的是 HeadFirst设计模式 这本书籍, 通过一些生动形象的例子, 把设计模式讲活了...


3.3、创造核心竞争力-不停留在只会用的地步


java开发往往离不开spring的生态系统, 框架开发出来就是给人更加方便开发功能用的, 如果仅仅会用, 那么在遇到一些问题的时候会无从下手, 三年半的时候, 我阅读了spring、springmvc、mybatis、springboot、springcloud等框架的源码, 通过书籍加视频的方式深入的了解了这些框架的原理, 看这些框架源码的时候, 不纠结于一些边线知识, 只管主线流程, 了解主线流程后, 我发现后续遇到问题时, 我能非常自信的跟进源码去排查问题, 在第四章节中我会整理每一个框架我都是通过哪些书籍来深入学习的


3.4、学以致用-尝试输出(github / 博客 / 分享)


学习一个知识, 如果仅仅看了一遍书 / 看了一遍视频, 那么可能过几天就会忘记了, 一般我是通过看视频 -> 记笔记 -> 看书 -> 对书中的知识点进行整理笔记, 笔记采用类似于给他人讲解的方式来记录 -> 将笔记记录在github 或者 以博客的形式分享出来, 在这样的链路下, 我每一步都能更加深刻的学习到知识点, 有时候看书看懂了不代表真懂了, 真正用笔记来描述的时候会发现是磕磕碰碰的, 与此同时, 将这些磕磕碰碰的知识去再次学习, 那么对整个知识点就会有更加全新的认识, 大家也可以看到, 我的掘金的博客是从2020年就开始写了, 都是我个人的口头描述转为文字描述


3.5、有枪不用和无枪可用


在掌握了工作中需要的知识点的情况下, 我们需要去学习流行的技术, 防止自己落伍, 技术的迭代更新是非常快的, 学习这些技术, 往往会给自己带来意想不到的结果


一年前公司我深入的去研究了eureka、zuul等springcloud组件的原理, 后面幸运的是, 公司有一个私有化部署的项目, 主管的计划是用微服务来搭建, 这个项目需要考虑到客户的资源,
有些客户可能预算比较高, 我们就可以提供一套完整的微服务来运行, 有些客户预算比较低, 那么可能最多就跑3-4个java项目, 于是主管的要求是我们的微服务功能, 需要能够满足上述的情况,
能够非常方便的将一个或者多个服务合并成一个服务, 并且自由搭配


正是因为我有对这些组件的深入了解, 我从源码层次提供了一套实现方案, 并且是最简单的实现方案, 主要的原理就是控制bean的加载(打包的时候一起打包, 但是不加载到内存)以及内部
rpc调用时的扩展(利用回环地址来尽可能的忽略http请求的花销), 如果我没有对这一块有所掌握, 那么我可能就失去了这样一个非常好的锻炼机会了


四、从成长的曲线来看侧重点


05.png


作者:zhongshenglong
来源:juejin.cn/post/7277489569958936588
收起阅读 »

茶百道全链路可观测实战

作者:山猎 茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会...
继续阅读 »

作者:山猎


茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会中,茶百道斩获“2021 成渝餐·饮标杆品牌奖”。2021 年 8 月,入选艾媒金榜(iiMedia Ranking)最新发布《2021 年上半年中国新式茶饮品牌排行 Top15》。2023 年 6 月 9 日,新茶饮品牌“茶百道”获得新一轮融资,由兰馨亚洲领投,多家知名投资机构跟投,估值飙升至 180 亿元。


今年 4 月,茶百道在成都总部举行了品牌升级发布会,宣布门店数突破 7000 家。根据中国连锁经营协会的数据,截至 2020 年、2021 年以及 2022 年 12 月 31 日,茶百道门店数量分别为 2,240 间、5,070 间以及 6,532 间,疫情并没有拖慢其扩张步伐。


随着业务规模的急速扩展,茶百道全面加速推进数字化转型战略。 但由于茶百道部分早期业务系统由外部 SaaS 服务商提供,无法满足线上业务高速增长所带来的大规模、高并发、弹性扩展、敏捷性、可观测等要求。为了满足线上线下门店客户需求与业务增长需要,针对店务、POS、用户交易、平台对接、门店管理、餐饮制作等核心链路服务,茶百道选择全面自研与阿里云云原生能力相结合,推动容器化、微服务化、可观测能力全面升级。


云原生化的业务价值


茶饮行业面临着市场竞争的压力和内部运营效率的提升需求。为了应对这些挑战,阿里云与茶百道一起完成云原生上云的转型,开启数字化的新征程。


采用容器和微服务技术实现了应用的轻量化和高可移植性。让企业可以更灵活地部署、扩展应用,快速响应市场需求,使得企业能够实现应用的高可用性和弹性扩展能力,无论面对突发的高峰访问量还是系统故障,都能保持业务的稳定运行。


引入了持续交付和持续集成的开发方式,帮助企业实现了快速迭代和部署。通过自动化的流程,企业能够更快地推出新功能和产品,与市场保持同步,抢占先机。


云原生的上云转型不仅带来了更高的安全性、可用性和可伸缩性,也提升了企业的创新能力和竞争力。


云原生带来的可观测挑战


茶百道作为业务高速发展的新兴餐饮品牌,每天都有海量的在线订单,这背后是与互联网技术的紧密结合,借助极高的数字化建设支撑茶百道庞大的销售量。因此,对于业务系统的连续性与可用性有着非常严苛的要求,以确保交易链路核心服务的稳定运行。特别是在每日高峰订餐时段、营销活动、突发热点事件期间,为了让用户有顺畅的使用体验,整个微服务系统的每个环节都需要保证在高并发大流量下的服务质量。


完善的全链路可观测平台以及 APM  ( Application Performance Management )工具,是保障业务连续性与可用性的前提。在可观测技术体系建设上,茶百道技术团队经历过比较多探索。全面实现容器化之前,茶百道在部分微服务系统上接入了开源 APM 工具,并进行超过一年时间的验证,但最终没有能够推广到整个微服务架构中,主要有这几个方面的原因:




  • 指标数据准确度与采样率之间的平衡难以取舍


    适当的采样策略是解决链路追踪工具成本与性能的重要手段,如果 APM 工具固定使用 100% 链路全采集,会带来大量重复链路信息被保存。在茶百道的庞大微服务系统规模下,100% 链路采集会造成可观测平台存储成本超出预期,而且在业务高峰期还会对微服务应用本身的性能带来一定影响。但开源工具在设定采样策略的情况下,又会影响指标数据准确度,使错误率、P99 响应时间等重要可观测指标失去观测与告警价值。




  • 缺少高阶告警能力


    开源工具在告警方面实现比较简单,用户需要自行分别搭建告警处理及告警分派平台,才能实现告警信息发送到 IM 群等基本功能。由于茶百道微服务化后的服务模块众多、依赖复杂。经常因为某个组件的异常或不可用导致整条链路产生大量冗余告警,形成告警风暴。造成的结果就是运维团队疲于应付五花八门且数量庞大的告警信息,非常容易遗漏真正用于故障排查的重要消息。




  • 故障排查手段单一


    开源 APM 工具主要基于 Trace 链路信息帮助用户实现故障定位,对于简单的微服务系统性能问题,用户能够快速找到性能瓶颈点或故障源。但实际生产环境中的很多疑难杂症,根本没有办法通过简单的链路分析去解决,比如 N+1 问题,内存 OOM,CPU 占用率过高,线程池打满等。这样就对技术团队提出了极高要求,团队需要深入了解底层技术细节,并具备丰富 SRE 经验的工程师,才能快速准确的定位故障根源。




接入阿里云应用实时监控服务 ARMS


在茶百道系统架构全面云原生化的过程中,茶百道技术团队与阿里云的工程师深入探讨了全链路可观测更好的落地方式。


ARMS 应用监控作为阿里云云原生可观测产品家族的重要成员,提供线程剖析、智能洞察、CPU & 内存诊断、告警集成等开源 APM 产品不具备的能力。在阿里云的建议下,茶百道技术团队尝试着将一个业务模块接入 ARMS 应用监控。


由于 ARMS 提供了容器服务 ACK 环境下的应用自动接入,只需要对每个应用的 YAML 文件增加 2 行代码就自动注入探针,完成整个接入流程。经过一段时间试用,ARMS 应用监控提供的实战价值被茶百道的工程师不断挖掘出来。茶百道同时使用了阿里云性能测试产品 PTS,来实现日常态和大促态的容量规划。因为ARMS和 PTS 的引入,茶百道日常运维与稳定性保障体系也发生了众多升级。


围绕 ARMS 告警平台构建应急响应体系


由于之前基于开源产品搭建告警平台时,经常遇到告警风暴的问题,茶百道对于告警规则的配置是非常谨慎的,尽可能将告警目标收敛到最严重的业务故障上,这样虽然可以避免告警风暴对 SRE 团队的频繁骚扰,但也会让很多有价值的信息被忽略,比如接口响应时间的突增等。


其实对于告警风暴问题,业界是有一整套标准解法的,其中涉及到去重、压缩、降噪、静默等关键技术,只是这些技术与可观测产品集成上存在一定复杂度,很多开源产品并没有在这个领域提供完善方案。


这些告警领域的关键技术,在 ARMS 告警平台上都有完整功能。以事件压缩举例,ARMS 提供基于标签压缩和基于时间压缩两种压缩方式。满足条件的多条事件会被自动压缩成为一条告警进行通知(如下图所示)。


图片
图: 基于标签压缩


图片
图:基于时间压缩


配合 ARMS 告警平台所提供的多种技术手段,可以非常有效的解决告警风暴的问题,因此茶百道技术团队开始重视告警的使用,逐步丰富更多的告警规则,覆盖应用接口、主机指标、JVM 参数、数据库访问等不同层面。


通过企业微信群进行对接,使告警通知实现 ISTM 流程的互动,当值班人员收到告警通知后,可以直接通过 IM 工具进行告警关闭、事件升级等能力,快速实现告警处理。(如下图所示)


图片
图:监控告警事件的智能化收敛与通告


灵活开放的告警事件处置策略满足了不同时效、场景的需求。茶百道在此基础上参考阿里巴巴安全生产最佳实践,开始构建企业级应急响应体系。将业务视角的应急场景作为事件应急处置的核心模型,通过不同告警级别,识别与流转对应的故障处理过程。这些都是茶百道在全面云原生化后摸索出的经验,并显著提升生产环境服务质量。


引入采样策略


从链路信息中提取指标数据,是所有 APM 工具的必备功能。不同于开源产品简单粗暴的指标提取方式,ARMS 应用监控使用端侧预聚合能力,捕捉每一次真实请求,先聚合,后采样,再上报,提供精准的指标监控。确保在采样策略开启的情况下,指标数据依然与真实情况保持一致。


图片
图:ARMS 端侧预聚合能力


为了降低 APM 工具带来的应用性能损耗,茶百道对大部分应用采取 10% 采样率,对于 TPS 非常高的应用则采取自适应采样策略,进一步降低高峰期应用性能损耗。通过实测,在业务高峰期,ARMS 应用监控造成的应用性能损耗比开源产品低 30% 以上且指标数据准确性可信赖, 比如接口级别的平均响应时间、错误数等指标都可以满足生产级业务需求。


图片
图:接口级别指标数据


异步链路自动埋点*


在 Java 领域存在异步线程池技术,以及众多开源异步框架,比如 RxJava、Reactor Netty、Vert.x 等。相较于同步链路,异步链路的自动埋点与上下文透传的技术难度更大。开源产品对主流异步框架的覆盖度不全,在特定场景下存在埋点失败问题,一旦出现这样的问题,APM 工具最重要的链路分析能力就难以发挥作用。


在这种情况下,需要开发者自行通过 SDK 手工埋点,以保证异步链路的上下文透传。这就会造成巨大的工作量且难以在团队内部大面积、快速推广。


ARMS 对主流的异步框架都实现了支持,无需任何业务代码上的侵入就能够异步链路上下文透传,即使对一些异步框架的特定版本没有及时支持,只要用户侧提出需求,ARMS 团队就能在新版本的探针中补齐。使用 ARMS 应用监控之后,茶百道技术团队直接将此前异步框架手工埋点代码进行了清理,大幅度减少维护工作量。


图片


图:异步调用的链路上下文


更高阶应用诊断技术的运用


在埋点覆盖度足够高的情况下,传统 APM 工具和链路跟踪工具能够帮助用户快速确定链路的哪一个环节(也就是Span)存在性能瓶颈,但需要更进一步排查问题根源时,就无法提供更有效的帮助了。


举一个例子,当系统 CPU 占用率显著提升时,是否因某个业务方法疯狂的消耗 CPU 资源所导致?这个问题对于大多数的 APM 产品而言,都是难以办法解决的。因为单从链路视图无法知晓每个环节的资源消耗情况。茶百道的工程师在使用开源工具时,曾多次遇到类似问题,当时只能凭借经验去猜测,再去测试环境反复对比来彻底解决,虽然也试过一些 Profiling 工具,但使用门槛比较高,效果不是很好。


ARMS 应用监控提供了 CPU & 内存诊断能力,可以有效发现 Java 程序中因为 CPU、内存和 I/O 导致的瓶颈问题,并按照方法名称、类名称、行号进行细分统计,最终协助开发者优化程序、降低延迟、增加吞吐、节约成本。CPU & 内存诊断可以在需要排查特定问题时临时开启,并通过火焰图帮助用户直接找到问题根源。在一次生产环境某应用 CPU 飙升场景中,茶百道的工程师通过 CPU & 内存诊断一步定位到问题是由一个特定业务算法所导致。


图片
图:通过火焰图分析 CPU 时间


此外,对于线上的业务问题,还可以通过 ARMS 提供的 Arthas 诊断能力在线排查。Arthas 作为诊断 Java 领域线上问题诊断利器,利用字节码增强技术,可以在不重启 JVM 进程的情况下,查看程序运行情况。


虽然 Arthas 使用有一定门槛,需要投入比较多精力进行学习,但茶百道的工程师非常喜欢使用这个工具。针对“到底符合哪种特殊的数据导致某业务异常”此类问题,没有比 Arthas 更方便的排查工具了。


图片


阶段性成果


经过 2 个月时间的调研与对比,茶百道决定全面从开源可观测平台转向 ARMS,从开源压测平台转向 PTS,并在团队内部进行推广。**随着使用的不断深入,ARMS 所提供的智能洞察、线程池分析等高阶可观测能力也逐步被茶百道的技术团队应用于日常运维中,线上问题排查效率相比之前也有了数倍提升。


在可观测产品本身的使用成本上,虽然表面上 ARMS 相比开源产品有所提高,但这是建立在开源方案数据单写,以及存在单点故障的情况下。其实茶百道的技术团队也非常清楚,之前的开源方案是存在高可用性隐患的,某个组件的故障会导致整个可观测方案不可用。只是大家对于开源方案提供的可观测能力并没有重度使用,所以才没有足够重视。所以综合来看,ARMS 整体成本并不会高于开源方案。


利用 ARMS 能力,茶百道实现了可观测指标采样率百分百覆盖,链路全采集,监控数据准确率大幅提供,能够快速实现业务故障的自动发现,有效的配合敏态业务发展。


故障发生后,监控系统需要第一时间通知相关人员,做初步定位,ARMS 告警告警能力实现了 ChatOps 能力,基于 IM 工具,快速触达相关人员,并且提供初步定位能力,是故障的响应能力大幅提升。


故障的快速恢复,对于控制业务影响至关重要,ARMS 利用全链路 Trace 能力,快速定位具体应用、接口、方法、慢sql等,是故障快速恢复的关键助手。茶百道技术团队负责人表示: “在与开源方案成本持平的前提下,ARMS 丰富且全面的全栈观测与告警能力,使茶百道快速建立运维观测与响应能力,故障恢复效率提升 50% 以上,故障恢复耗时****缩短 50%,真正做到用可观测为业务迅猛发展保驾护航。”


故障的预防收敛,在稳定性体系建设中是投入产出比极高的,PTS 利用全国流量施压的能力,和秒级监控能力,验证站点容量并定位性能瓶颈。茶百道在业务上线前,充分对单应用和全链路做压测,累计压测 800 余次,在上线前做到了性能问题的收敛,避免演进为线上故障。


下阶段目标


在可观测领域,Prometheus + Grafana 是指标数据存储、计算、查询、展示的事实标准,ARMS 产品家族提供托管加强的 Prometheus 和 Grafana 服务。ARMS 应用监控生成的指标数据也会自动保存到托管版 Prometheus 中,并预置数张 Grafana 大盘。茶百道的工程师们正在基于 Prometheus 和 Grafana,将应用层指标、关键业务指标、云服务指标进行结合,开发多维度可观测大盘。


在不久的将来,茶百道就会建立覆盖业务层、用户体验层、应用服务层、基础设置层、云服务层的统一可观测技术体系,为千万级用户同时在线的大规模微服务系统实现稳定性保障。


作者:阿里云云原生
来源:juejin.cn/post/7289767547329970231
收起阅读 »

领导说我工作 3 年了只会 CRUD

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。 领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。 项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。 队友建议以短、平、快为主,能够使用已有现成...
继续阅读 »

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。


领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。


项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。


队友建议以短、平、快为主,能够使用已有现成的技术就用现成的技术。直接面向过程开发是人们最为舒适,是人为本能的习惯。由于他有这一种能够处理好的决心,便把数据批量操作这块委托于他。


查看了以往公司现成一些写法,一部分是直接面向 SQL 写法批量插入,面对增量同步则先查出,存在的更新,不存在的插入。一部分是通过 Kafka 和后台任务原子操作。


理论上这么操作结果也能成,但是看到修改记录,我就知道面临的需求变了很多变化很快,导致大量的更改。私底下询问负责人也了解出了太多问题,原本一劳永逸赶紧写完结果反而投入了更多的精力和时间。


出于预防心理,也对那位同事进行了提醒并且加以思考再下手。


不到一个月,我们就把项目上线了,并且没有出现数据上的错误,得到了领导的表扬。


我们也提前收场,做一些小的优化,其余时间在摸鱼。


一段时间之后,麻烦便接踵而至,其一就是开始数据量暴增,那位同事在做增量同步时进行了锁表操作,批量操作需要一些时间,在前台读取时出现响应超时。


其二就是增量同步要调整,以主库或第三方来源库为主,出现数据更新和删除的需要同步操作。


同事目前的主力放在了新项目上,把一些零散的时间用来调整需求和 bug,结果越处理,bug 出现的越多,不是数量过多卡死就是变量不对导致数据处理不对。


于是到了某一时刻终于爆发,领导找到我俩,被痛批一顿,工作这么久就只会 CRUD 操作,来的实习生都会干的活,还养你们干什么。


当然,要复盘的话当然有迹可循。我想碰见这种情况还真不少,首次开发项目时一鼓作气,以“短、平、快” 战术面向过程开发,短时间内上线。


但是,一个软件的生命周期可不止步于上线,还要过程运维以及面对变化。


导致在二次开发的时候就脱节了,要么当时写法不符合现有业务,要么改动太多动不动就割到了大动脉大出血,要么人跑了...


所以我们会采用面向对象,抽象化编程,就是用来保稳定,预留一部分来应付变化,避免牵一发而动全身。


挨完骂,也要开始收拾烂摊子。


于是我打算重新组装一个通用的方法,打算一劳永逸。


首先我们定义一个接口通用思维 IDbAsyncBulk。由于源码已经发布到了github,所以一些注释写成了英文,大致也能看出蹩脚英文的注释。


public interface IDbAsyncBulk
    {
        /// <summary>
        /// default init.
        /// use reflect to auto init all type, to lower case database fileds,and  default basic type.
        /// if ignore some fileds,please use DbBulk,Ignore property to remarkable fileds.
        /// if other operating,need user-defined to init operate.
        /// </summary>
        /// <typeparam name="T">Corresponding type</typeparam>
        Task InitDefaultMappings<T>();

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection_string">database connecting string.</param>
        /// <param name="targetTable">target table name. </param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(string connection_string, string targetTable, List<T> list);

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.
        /// if nothing other follow-up operate, shouldn't cover this connecting.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(DbConnection connection, string targetTable, List<T> list);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        Task UpdateToServer<T>(string connection_string, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="createtemp"> create temporary table or not </param>
        Task UpdateToServer<T>(DbConnection connection, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = nullbool createtemp = true);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        ///  1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// create temporary table
        /// </summary>
        /// <param name="tempTable">create temporary table name</param>
        /// <param name="targetTable">rarget table name</param>
        /// <param name="connection">database connecting</param>
        Task CreateTempTable(string tempTable, string targetTable, DbConnection connection);
    }

解释几个方法的作用:



InitDefaultMappings:初始化映射,将目标表的字段映射到实体,在批量操作时候会根据反射进行一一匹配表字段;


CopyToServer:批量新增,在符合数据表结构时批量复制到目标表,采用官方 SqlBulkCopy 类结合实体简化操作。


MergeToServer:增量同步,需指定唯一键,存在即更新,不存在则插入。支持指定更新字段,指定插入字段。


UpdateToServer:批量更新,需指定 where 条件,以及更新的字段。


MergeAndDeleteToServer:增量同步,以数据源和目标表进行匹配,目标表存在的则更新,不存在的则插入,目标表存在,数据源不存在则目标表移除。


CreateTempTable:创建临时表。



增加实体属性标记,用来标记列名是否忽略同步数据,以及消除数据库别名,大小写的差异。


 /// <summary>
    /// 数据库批量操作标记,用于标记对象属性。
    /// </summary>
    public class DbBulkAttribute : Attribute
    {
        /// <summary>
        /// 是否忽略。忽略则其余属性不需要设置,不忽略则必须设置Type。
        /// </summary>
        public bool Ignore { getset; }

        /// <summary>
        /// 列名,不设置则默认为实体字段名小写
        /// </summary>
        public string ColumnName { getset; }

    }

实现类,目前仅支持 SqlServer 数据库,正在更新 MySql 和 PGSql 中。然后需要定义BatchSize(default 10000)、BulkCopyTimeout (default 300)、ColumnMappings,分别是每批次大小,允许超时时间和映射的字段。


/// <summary>
    /// sql server batch
    /// </summary>
    public class SqlServerAsyncBulk : IDbAsyncBulk
    {
        /// <summary>
        /// log recoding
        /// </summary>
        private ILogger _log;
        /// <summary>
        ///batch insert size(handle a batch every time )。default 10000。
        /// </summary>
        public int BatchSize { getset; }
        /// <summary>
        /// overtime,default 300
        /// </summary>
        public int BulkCopyTimeout { getset; }
        /// <summary>
        /// columns mapping
        /// </summary>
        public Dictionary<stringstring> ColumnMappings { getset; }
        /// <summary>
        /// structure function
        /// </summary>
        /// <param name="log"></param>
        public SqlServerAsyncBulk(ILogger<SqlServerAsyncBulk> log)
        {
            _log = log;
            BatchSize = 10000;
            BulkCopyTimeout = 300;
        }
        
        //...to do

使用上也非常的简便,直接在服务里注册单例模式,使用的时候直接依赖注入。


 //if you use SqlServer database, config SqlServerAsyncBulk service.
services.AddSingleton<IDbAsyncBulk, SqlServerAsyncBulk>();

public class BatchOperate
{
  private readonly IDbAsyncBulk _bulk;
  public BatchOperate(IDbAsyncBulk bulk)
  {
    _bulk = bulk;
  }
}

以 user_base 表举两个实例,目前测试几十万数据也才零点几秒。


 public async Task CopyToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=xxx;Password=xxx";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++) {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "xiaoyu@163.com",
                    name = $"小榆{i}",
                    nation = "
",
                    nationality="
中国"
                });
            }
            await _bulk.CopyToServer(connectStr, "
user_base", mock_list);
        }

public async Task MergeToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=sa;Password=root";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++)
            {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "mock@163.com",
                    name = $"小榆{i}",
                    nation = "汉",
                    nationality = "中国"
                });
            }
            var insertMapping = new List<string> { "birthday""education""age""email""name""nation""nationality" };
            var updateMapping = new List<string> { "birthday""education""age""email"};
            await _bulk.MergeToServer(connectStr,new List<string> {"id"}, "user_base", mock_list,null, insertMapping, updateMapping);
        

到这里,也已经完成了批量数据操作啦,不用再面对大量的sql操作啦。面向 sql 开发一时确实爽,但是面临变化或者别人接手的时候,是很痛苦的。


具体实现细节内容过多,篇幅有限暂时不全部展示,有兴趣或者尝试的伙伴可以进 github 进行参考。



github👉:github.com/sangxiaoyu/… 💖



作者:桑小榆呀
来源:juejin.cn/post/7290361767141376057
收起阅读 »

为网站配置SSL

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加...
继续阅读 »

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间),这个系统提供了身份验证与加密通讯方法。



现状

















证书申请




除了向公有云申请证书, 也可使用 自签名openssl生成的证书,但方便起见还是使用云厂商提供的证书.








一般免费版,只有一年有效期.到期需要重新申请&更换














Nginx配置




将证书文件上传至/usr/local/openresty/nginx/conf/cert目录下.




博客项目当前的conf配置如下:


server {
    listen      80;
    server_name dashen.tech www.dashen.tech;
    access_log  /var/log/blog.access.log main;
    error_log  /var/log/blog.error.log;

  location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }
}


新增启用https的配置:
server {
      listen        443 ssl;                                                 
      server_name    dashen.tech www.dashen.tech;  #域名                         
      ssl_certificate      /usr/local/openresty/nginx/conf/cert/shuang_blog.pem;  #证书路径     
      ssl_certificate_key  /usr/local/openresty/nginx/conf/cert/shuang_blog.key;  #key路径             
      ssl_session_cache    shared:SSL:1m;   #s储存SSL会话的缓存类型和大小                       
      ssl_session_timeout  5m; #会话过期时间 

      access_log  /var/log/blog.access.log main;
      error_log  /var/log/blog.error.log;

location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }                                                     
  }



删掉之前的conf. 重启nginx,访问https://www.dashen.tech[1],已能正常访问.




再访问之前的网址https://dashen.tech[2],则







配置将http访问自动跳转到https




再增加一段配置:


server {
    listen      80;
    server_name dashen.tech www.dashen.tech;
    access_log  /var/log/blog.access.log main;
    error_log  /var/log/blog.error.log;

    return      301 https://$server_name$request_uri; #这是nginx最新支持的写法

  location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }
}

参考: Nginx强制跳转Https[3]


再次重启nginx,这时请求https://dashen.tech[4]就可以跳转到https://www.dashen.tech[5]




但因为网站下有部分资源使用了http,所以浏览器依然没有变为安全锁,


可参考Hexo启用https加密连接[6],


也可右键查看哪些请求使用了http,将其修改为https即可~





参考资料


[1]

https://www.dashen.tech: https://www.dashen.tech

[2]

https://dashen.tech: https://dashen.tech

[3]

Nginx强制跳转Https: https://www.jianshu.com/p/116fc2d08165

[4]

https://dashen.tech: https://dashen.tech

[5]

https://www.dashen.tech: https://www.dashen.tech

[6]

Hexo启用https加密连接: https://note.youdao.com/web/#/file/recent/note/WEBe69d252eb353dd5ee0210d053ec0cc3a/



作者:fliter
来源:mdnice.com/writing/3257fabc35eb44a7a9be93bd809ffeca
收起阅读 »

lstio在微服务框架中的使用

在云原生时代,微服务架构已经成为企业构建灵活、可扩展和高可用系统的首选方案。但是,微服务也带来了一系列新的挑战,包括服务发现、负载均衡、安全、监控等。Istio是一款开源的服务网格,它通过提供丰富的特性帮助开发者轻松应对这些挑战。在本文中,我们将探索Istio...
继续阅读 »

在云原生时代,微服务架构已经成为企业构建灵活、可扩展和高可用系统的首选方案。但是,微服务也带来了一系列新的挑战,包括服务发现、负载均衡、安全、监控等。Istio是一款开源的服务网格,它通过提供丰富的特性帮助开发者轻松应对这些挑战。在本文中,我们将探索Istio在微服务框架中的使用原理,并深入分析与Spring Cloud的集成案例。


Istio的使用原理


Istio通过将智能代理(Envoy)注入到每个微服务的Pod中,从而实现微服务之间的网络通信的拦截和管理。Envoy代理负责处理服务与服务之间的交互,这样就能在不修改微服务业务代码的情况下,实现流量管理、安全、监控等功能。


源码结构


Istio的源码由Go语言编写,主要包括以下组件:



  1. Envoy Proxy:由C++编写,负责流量的代理和管理。

  2. Pilot:提供服务发现和流量管理功能。

  3. Mixer:负责策略控制和遥测数据收集。

  4. Citadel:提供服务间通信的安全认证和授权功能。


实例:Istio与Spring Cloud集成


以下是一个简单的示例,展示如何在Spring Cloud微服务中使用Istio。


在Istio和Spring Cloud的集成场景中,你需要在Kubernetes集群中部署Spring Cloud应用,并且确保Istio的Envoy代理被注入到应用的Pod中。以下是一个详细步骤和配置说明。


1. 安装Istio


首先确保你已经在Kubernetes集群中安装了Istio并启用了自动sidecar注入。如果还没有安装,可以按照Istio的官方文档进行安装和配置。


2. 准备Spring Cloud应用的Docker镜像


确保你的Spring Cloud应用已经被打包为Docker镜像,并推送到Docker镜像仓库中。例如:


docker build -t myrepo/springcloud-service:v1 .
docker push myrepo/springcloud-service:v1

3. 创建Kubernetes Deployment配置文件**


创建一个YAML配置文件,用于部署Spring Cloud应用。注意,我们在Pod的metadata.annotations中添加了sidecar.istio.io/inject: "true",用于启用Istio sidecar自动注入。


例如,创建一个名为springcloud-service-deployment.yaml的文件,内容如下:


apiVersion: apps/v1
kind: Deployment
metadata:
name: springcloud-service
spec:
replicas: 3
selector:
matchLabels:
app: springcloud-service
template:
metadata:
labels:
app: springcloud-service
annotations:
sidecar.istio.io/inject: "true" # 启用Istio sidecar自动注入
spec:
containers:
- name: springcloud-service
image: myrepo/springcloud-service:v1 # 使用你的Spring Cloud应用镜像
ports:
- containerPort: 8080 # 应用的端口号

4. 部署Spring Cloud应用到Kubernetes集群**


使用kubectl命令行工具部署应用:


kubectl apply -f springcloud-service-deployment.yaml

这会在Kubernetes集群中创建一个新的Deployment,运行你的Spring Cloud应用,并且每个Pod中都会注入Istio的Envoy代理。


5. 检查部署状态**


使用以下命令查看Pod的状态,确保所有Pod都已经正常运行,并且Istio的Envoy代理也被正确注入。


kubectl get pods

你应该能看到类似如下的输出,其中2/2表示每个Pod中有两个容器(你的应用和Istio Envoy代理)都已经正常运行。


NAME                                       READY   STATUS    RESTARTS   AGE
springcloud-service-5c79df6f59-9fclp 2/2 Running 0 5m
springcloud-service-5c79df6f59-g6qlc 2/2 Running 0 5m
springcloud-service-5c79df6f59-wvndz 2/2 Running 0 5m

至此,你的Spring Cloud应用已经被成功部署在Istio服务网格中,可以利用Istio提供的各种特性来管理和监控应用的运行。


配置Istio资源


当你的 Spring Cloud 应用已经成功部署在 Istio 服务网格中后,你需要配置 Istio 资源以管理服务间的流量、安全、策略和遥测等功能。以下我们将具体说明如何配置 Istio 资源,主要涉及 VirtualService 和 DestinationRule。


1. VirtualService**


VirtualService 定义了访问一个服务的路由规则。你可以控制根据不同的请求属性(例如 URL、请求头等)将流量路由到不同的服务或服务的不同版本。


创建 VirtualService 配置文件


以下是一个 virtual-service.yaml 的示例,该文件定义了一个简单的路由规则,将所有发送到 springcloud-service 服务的流量路由到标有 "v1" 标签的 Pod。


apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: springcloud-service
spec:
hosts:
- springcloud-service
http:
- route:
- destination:
host: springcloud-service
subset: v1

这里 hosts 定义了这个 VirtualService 的作用域,即它将控制哪些服务的流量。http 定义了 HTTP 流量的路由规则,destination 定义了匹配的流量将被路由到哪里。


2. DestinationRule


DestinationRule 定义了 Pod 的子集和对这些子集的流量的策略。通常和 VirtualService 一起使用,用于细粒度控制流量。


创建 DestinationRule 配置文件


以下是一个 destination-rule.yaml 的示例,定义了 springcloud-service 服务的两个子集:v1 和 v2。


apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: springcloud-service
spec:
host: springcloud-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2

在这里,我们定义了两个子集 v1 和 v2,分别匹配标签为 version=v1version=v2 的 Pod。这样你就可以在 VirtualService 中使用这些子集来控制流量。


应用 Istio 配置


将 VirtualService 和 DestinationRule 的配置文件应用到 Kubernetes 集群中:


kubectl apply -f virtual-service.yaml
kubectl apply -f destination-rule.yaml

通过配置 Istio 的 VirtualService 和 DestinationRule,您可以轻松控制和管理在 Istio 服务网格中运行的 Spring Cloud 应用的流量。你可以实现各种高级的流量管理功能,如金丝雀发布、蓝绿部署、流量镜像、故障注入等,而无需更改应用的代码。


代码示例


下面是一个基于Spring Cloud的简单微服务应用代码示例。


@RestController
public class HelloWorldController {

@RequestMapping("/hello")
public String hello() {
return "Hello, World!";
}

}

在Istio环境中,你不需要更改Spring Cloud应用的代码。Envoy代理会自动处理服务间的通信,你只需要使用Istio的配置文件定义流量路由规则、策略等。


总结


Istio与Spring Cloud的结合为开发者提供了一种强大的方式来部署、管理和扩展微服务应用。Istio的流量管理、安全认证和遥测数据收集功能使得开发者能够更加关注业务逻辑的开发,而不是底层的网络通信和安全问题。希望这个更详细的指南能帮助你更好地理解Istio与Spring Cloud的集成。


作者:一只爱撸猫的程序猿
来源:juejin.cn/post/7290485584069001231
收起阅读 »

从拼夕夕砍一刀链接漫谈微信落地页防封

写在前面 最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。 是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在p...
继续阅读 »

写在前面


最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。


image.png
是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在pc端浏览器打开时,则出现的另外一套界面。是什么导致了这样的情况呢?

微信落地页防封


谈到拼多多的短链分享,就不得不提一个很关键的名词微信落地页防封 ,说到微信落地页防封,那就需要知道,在什么情况下,会触发微信的域名拦截机制,一般来说,触发域名拦截有以下几个原因




  • 域名是新购入的老域名,在微信内之前有过违规记录,上过黑名单。




  • 网站流量太大,微信内同一域名被大量分享,比如分享赚类的平台某拼。




  • 诱导分享传播,即便是合法营销活动,也会触发拦截。




  • 网站内容违规,这个不必多说。




  • 被同行恶意举报。




为了让域名活的久一些,微信落地页防封这样的技术就应运而生,主要通过以下几点,来逃避微信的域名拦截机制



  • 大站域名【美团、京东...】

  • 不同主体各自备案域名【鸡蛋不放在一个篮子内】

  • 多级跳转+前置防火墙【通过前置防火墙中转页识别是否是机器扫描】

  • 随机Ip【cdn分发】

  • 图床 + 短链

  • 短链 + 自定义跳转 【稍后详细分析一下这种方式】


拼夕夕的防封技术猜测


经过测试,拼夕夕的防封应该采用的是图床+短链+自定义跳转的方式,接下来就听我一一道来



  • 图床
    图床是oss对象存储的昵称,通常是用来存放图片的,如果是用在防封里,那他其实是将一个html页面上传进了图床内,至于是怎么上传进去的。很简单啊,你只需要有一个阿里云,京东云,腾讯云的账号,购买了oss对象存储服务,设置公共读私有写,就可以访问了,这些不重要,你只需要知道图床所存储的是html就可以了。


我通过chrome的控制台抓取了通过短链转换而来地址,然后抓到了如下请求



  • 短链重定向


image.png
注意看第一个请求,第一个请求就是短链的自定义跳转,短链自定义跳转我们下一节详细去说,通过301重定向,将我们重定向到了图床的地址



  • 图床ua、地域、等判断
    图床内的html包含了对ua、地域、设备类型等的判断,不同的环境所打开的内容是不同的,通过对环境的判断,展示不同的内容去屏蔽微信的扫描,拼夕夕就是通过这样的方式来实现落地页防封的
    下面是我从落地页中拿到的一个函数,虽然我们很难完全还原这个函数,但是通过里面没被混淆的常量比如ke.HUAWEIke.OPPO等不难看出来,这是一个判断当前手机品牌的函数,针对不同的品牌下的浏览器,会做一些特殊的处理。


 const t = e(u().mark(function t (e) {
let r, n, o, i, c, s
return u().wrap(function (t) {
for (; ;) {
switch (t.prev = t.next) {
case 0:
if (r = e.brand,
n = e.payload,
o = a()(n, 'data', {}),
i = a()(n, 'isThirdBrowser'),
c = a()(n, 'data.fastAppDomains', ''),
s = Te(o),
Pe(o),
!i) {
t.next = 8
break
}
return t.abrupt('return')
case 8:
if (r !== ke.HUAWEI) {
t.next = 11
break
}
return t.next = 11,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 11:
if (r !== ke.OPPO) {
t.next = 27
break
}
if (!j(A.OppoLeftScreen, o)) {
t.next = 17
break
}
return t.next = 15,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 15:
case 20:
t.next = 27
break
case 17:
return t.prev = 17,
t.next = 20,
sn(c, {
cTime: s,
data: o
})
case 22:
if (t.prev = 22,
t.t0 = t.catch(17),
!j(A.banBrowserV2, o) && !j(A.oppoQAppPriority, o)) {
t.next = 27
break
}
return t.next = 27,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 27:
if (r !== ke.VIVO) {
t.next = 30
break
}
return t.next = 30,
sn(c, {
cTime: s,
data: o
}).catch(fn)
case 30:
case 'end':
return t.stop()
}
}
}
, t, null, [[17, 22]])
}
))

再注意看接下来的一段代码片段,很明显针对上面获取到的手机品牌,会生成不同的图片,注意看下面混淆过的c函数,x.brandType, brand有品牌的意思,也就是上面函数获取到的手机品牌


o = new Promise((function(t) {
var r, o = document.createElement("img"), i = k(n), c = (f(r = {}, x.brandType, 1),
f(r, E.funcParams, i),
r), u = a()(e.split(","), "0");
o.onload = function(e) {
var r = a()(e, "path[0]") || a()(e, "target")
, n = gn(r);
t({
brand: n,
img: r
})
}
,
o.onerror = function() {
t({
brand: ke.OTHERS
})
}
;
var s = S(u).href;
o.src = m(c, s)
}
)),

得益于落地页开发者优秀的代码命名习惯,通过下面的片段,isWeChatPlatform,isIOSWeChatPlatform这两个字符串让我们知道落地页里面还有针对微信的一些判断,会判断是安卓还是ios微信


n = a()(r, "data", {}),
i = a()(r, "isWeChatPlatform"),
c = a()(r, "isIOSWeChatPlatform"),
f = a()(r, "data.mqCodeKey", ""),
l = a()(r, "data.websiteDomain", "").replace(/\/$/, ""),
p = a()(r, "data.fastAppDomains", ""),
d = v("image_url"),
h = v(f) || location.href,
!d) {
t.next = 15;
break
}

还有落地页内针对UA的判断的实现


((w = t.document),
(x = w ? w.title : ''),
(_ = navigator.userAgent.toLowerCase()),
(S = navigator.platform.toLowerCase()),
(O = !(!S.match('mac') && !S.match('win'))),
(A = _.indexOf('wxdebugger') != -1),
(E = _.indexOf('micromessenger') != -1),
(I = _.indexOf('android') != -1),
(T = _.indexOf('iphone') != -1 || _.indexOf('ipad') != -1),
(P = function () {
const t = _.match(/micromessenger\/(\d+\.\d+\.\d+)/) || _.match(/micromessenger\/(\d+\.\d+)/)
return t ? t[1] : ''

通过上面的代码片段,我们得以窥见拼夕夕落地页的逻辑设计,落地页内,至少实现了下面的能力



  • 针对手机品牌的处理

  • 针对安卓与ios系统的处理

  • 针对是否微信的处理


这些代码进一步的验证了我们的猜想,拼夕夕的确是通过oss内的html动态创建元素来规避微信拦截的!下面是短链智能跳转的一个例子,可以帮助大家更好的理解短链推广的内在逻辑


短链与智能跳转


我们以某平台的功能为例,演示如何通过短链实现自定义的跳转



  • 创建短链接


image.png



  • 配置智能跳转


image.png



  • 智能跳转的规则


可以看到,本身规则就支持按平台,按访问环境,按地域去进行智能跳转了,这也是为什么谷歌会想要将UA的信息进行加密或减少所提供的的信息。


image.png



  • 按地域的实现
    服务器可以看到当前访问的ip,通过ip去反向推断地域

  • 操作系统、访问环境 是通过判断UA来实现


console.log(navigator.userAgent)
// ua内会包含设备的关键信息,如果是微信浏览器内打开的,会携带微信浏览器特有的ua信息
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'

结语



技术本身都是为了解决现实存在的问题,技术没有好坏黑白,但是作为一个技术人,我们能做的就是做任何事情的时候,要坚守心中的底线。君子不立危墙之下,尽量少游走在黑白间的灰色地带。



作者:AprilKroc
来源:juejin.cn/post/7156548454502629384
收起阅读 »

我说ArrayList初始容量是10,面试官让我回去等通知

引言 在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。 ArrayList的初始容量是多少?(90%的人都会答错) ArrayL...
继续阅读 »

引言


在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。



  1. ArrayList的初始容量是多少?(90%的人都会答错)

  2. ArrayList的扩容机制

  3. 并发修改ArrayList元素会有什么问题

  4. 如何快速安全的删除ArrayList中的元素


接下来一块分析一下ArrayList的源码,看完ArrayList源码之后,可以轻松解答上面四个问题。


简介


ArrayList底层基于数组实现,可以随机访问,内部使用一个Object数组来保存元素。它维护了一个 elementData 数组和一个 size 字段,elementData数组用来存放元素,size字段用于记录元素个数。它允许元素是null,可以动态扩容。
image.png


初始化


当我们调用ArrayList的构造方法的时候,底层实现逻辑是什么样的?


// 调用无参构造方法,初始化ArrayList
List<Integer> list1 = new ArraryList<>();

// 调用有参构造方法,初始化ArrayList,指定容量为10
List<Integer> list1 = new ArraryList<>(10);

看一下底层源码实现:


// 默认容量大小
private static final int DEFAULT_CAPACITY = 10;

// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认容量的数组对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储元素的数组
transient Object[] elementData;

// 数组中元素个数,默认是0
private int size;

// 无参初始化,默认是空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 有参初始化,指定容量大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 直接使用指定的容量大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}

可以看到当我们调用ArrayList的无参构造方法 new ArraryList<>() 的时候,只是初始化了一个空对象,并没有指定数组大小,所以初始容量是零。至于什么时候指定数组大小,接着往下看。


添加元素


再看一下往ArrayList种添加元素时,调用的 add() 方法源码:


// 添加元素
public boolean add(E e) {
// 确保数组容量够用,size是元素个数
ensureCapacityInternal(size + 1);
// 直接在下个位置赋值
elementData[size++] = e;
return true;
}

// 确保数组容量够用
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算所需最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果数组等于空数组,就设置默认容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

// 确保容量够用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于数组长度,就进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

看一下扩容逻辑:


// 扩容,就是把旧数据拷贝到新数组里面
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 计算新数组的容量大小,是旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

// 如果扩容后的容量小于最小容量,扩容后的容量就等于最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

// 如果扩容后的容量大于Integer的最大值,就用Integer最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);

// 扩容并赋值给原数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到:



  • 扩容的触发条件是数组全部被占满

  • 扩容是以旧容量的1.5倍扩容,并不是2倍扩容

  • 最大容量是Integer的最大值

  • 添加元素时,没有对元素校验,允许为null,也允许元素重复。


再看一下数组拷贝的逻辑,这里都是Arrays类里面的方法了:


/**
* @param original 原数组
* @param newLength 新的容量大小
*/

public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个新数组,容量是新的容量大小
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 把原数组的元素拷贝到新数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

最终调用了System类的数组拷贝方法,是native方法:


/**
* @param src 原数组
* @param srcPos 原数组的开始位置
* @param dest 目标数组
* @param destPos 目标数组的开始位置
* @param length 被拷贝的长度
*/

public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length)
;

总结一下ArrayList的 add() 方法的逻辑:



  1. 检查容量是否够用,如果够用,直接在下一个位置赋值结束。

  2. 如果是第一次添加元素,则设置容量默认大小为10。

  3. 如果不是第一次添加元素,并且容量不够用,则执行扩容操作。扩容就是创建一个新数组,容量是原数组的1.5倍,再把原数组的元素拷贝到新数组,最后用新数组对象覆盖原数组。


需要注意的是,每次扩容都会创建新数组和拷贝数组,会有一定的时间和空间开销。在创建ArrayList的时候,如果我们可以提前预估元素的数量,最好通过有参构造函数,设置一个合适的初始容量,以减少动态扩容的次数。


删除单个元素


再看一下删除元素的方法 remove() 的源码:


public boolean remove(Object o) {
// 判断要删除的元素是否为null
if (o == null) {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

// 删除该位置上的元素
private void fastRemove(int index) {
modCount++;
// 计算需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 从index+1位置开始拷贝,也就是后面的元素整体向左移动一个位置
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 设置数组最后一个元素赋值为null,防止会导致内存泄漏
elementData[--size] = null;
}

删除元素的流程是:



  1. 判断要删除的元素是否为null,如果为null,则遍历数组,使用双等号比较元素是否相等。如果不是null,则使用 equals() 方法比较元素是否相等。这里就显得啰嗦了,可以使用 Objects.equals()方法,合并ifelse逻辑。

  2. 如果找到相等的元素,则把后面位置的所有元素整体相左移动一个位置,并把数组最后一个元素赋值为null结束。


可以看到遍历数组的时候,找到相等的元素,删除就结束了。如果ArrayList中存在重复元素,也只会删除其中一个元素。


批量删除


再看一下批量删除元素方法 removeAll() 的源码:


// 批量删除ArrayList和集合c都存在的元素
public boolean removeAll(Collection<?> c) {
// 非空校验
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement){
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 把需要保留的元素左移
elementData[w++] = elementData[r];
} finally {
// 当出现异常情况的时候,可能不相等
if (r != size) {
// 可能是其它线程添加了元素,把新增的元素也左移
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 把不需要保留的元素设置为null
if (w != size) {
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

批量删除元素的逻辑,并不是大家想象的:



遍历数组,判断要删除的集合中是否包含当前元素,如果包含就删除当前元素。删除的流程就是把后面位置的所有元素整体左移,然后把最后位置的元素设置为null。



这样删除的操作,涉及到多次的数组拷贝,性能较差,而且还存在并发修改的问题,就是一边遍历,一边更新原数组。
批量删除元素的逻辑,设计充满了巧思,具体流程就是:



  1. 把需要保留的元素移动到数组左边,使用下标 w 做统计,下标 w 左边的是需要保留的元素,下标 w 右边的是需要删除的元素。

  2. 虽然ArrayList不是线程安全的,也考虑了并发修改的问题。如果上面过程中,有其他线程新增了元素,把新增的元素也移动到数组左边。

  3. 最后把数组中下标 w 右边的元素都设置为null。


所以当需要批量删除元素的时候,尽量使用 removeAll() 方法,性能更好。


并发修改的问题


当遍历ArrayList的过程中,同时增删ArrayList中的元素,会发生什么情况?测试一下:


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 遍历ArrayList
for (Integer key : list) {
// 判断如果元素等于2,则删除
if (key.equals(2)) {
list.remove(key);
}
}
}
}

运行结果:


Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.yideng.Test.main(Test.java:14)

报出了并发修改的错误,ConcurrentModificationException
这是因为 forEach 使用了ArrayList内置的迭代器,这个迭代器在迭代的过程中,会校验修改次数 modCount,如果 modCount 被修改过,则抛出ConcurrentModificationException异常,快速失败,避免出现不可预料的结果。


// ArrayList内置的迭代器
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;

// 迭代下个元素
public E next() {
// 校验 modCount
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E)elementData[lastRet = i];
}

// 校验 modCount 是否被修改过
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

如果想要安全的删除某个元素,可以使用 remove(int index) 或者 removeIf() 方法。


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 使用 remove(int index) 删除元素
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(2)) {
list.remove(i);
}
}

// 使用removeIf删除元素
list.removeIf(key -> key.equals(2));
}

}

总结


现在可以回答文章开头提出的问题了吧:



  1. ArrayList的初始容量是多少?


答案:初始容量是0,在第一次添加元素的时候,才会设置容量为10。



  1. ArrayList的扩容机制


答案:



  1. 创建新数组,容量是原来的1.5倍。

  2. 把旧数组元素拷贝到新数组中

  3. 使用新数组覆盖旧数组对象

  4. 并发修改ArrayList元素会有什么问题


答案:会快速失败,抛出ConcurrentModificationException异常。



  1. 如何快速安全的删除ArrayList中的元素


答案:使用remove(int index)removeIf() 或者 removeAll() 方法。
我们知道ArrayList并不是线程安全的,原因是它的 add()remove() 方法、扩容操作都没有加锁,多个线程并发操作ArrayList的时候,会出现数据不一致的情况。
想要线程安全,其中一种方式是初始化ArrayList的时候使用 Collections.synchronizedCollection() 修饰。这样ArrayList所有操作都变成同步操作,性能较差。还有一种性能较好,又能保证线程安全的方式是使用 CopyOnWriteArrayList,就是下章要讲的。


// 第一种方式,使用 Collections.synchronizedCollection() 修饰
List<Integer> list1 = Collections.synchronizedCollection(new ArrayList<>());

// 第二种方式,使用 CopyOnWriteArrayList
List<Integer> list1 = new CopyOnWriteArrayList<>();

作者:一灯架构
来源:juejin.cn/post/7288963211071094842
收起阅读 »

10分钟3个步骤集成使用SkyWalking

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功...
继续阅读 »

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功能。使用简便、上手又快。真可谓快、准、狠。


本文主要介绍如何快速集成使用SkyWalking,从3个方面入手:原理、搭建、使用。


1、原理


1.1、概括


SkyWalking整体分为4个部分:探针采集层、数据传输和逻辑处理层、数据存储层、数据展示层。



1.2、探针采集层


所谓探针,实际上是一种动态代理技术,只不过不是我们常用的Java代理类,而是在类加载时,就生成了增强过的代理类的字节码,增强了数据拦截采集上报的功能。


探针技术是在项目启动时通过字节码技术(比如JavaAgent、ByteBuddy)进行类加载和替换,生成新的增强过的Class文件,对性能的影响是一次性的。


探针技术,因为在类加载时进行转换,增强了部分功能,所以会增加项目启动时间,同时也会增加内存占用量和线程数量。但是对性能影响不大,官方介绍在5% ~ 10%之间。



探针层在类转换时,通过各种插件对原有的类进行增强,之后在运行时拦截请求,然后将拦截的数据上报给Skywalking服务端。同时再加上一些定时任务,去采集应用服务器的基础数据,比如JVM信息等。


1.3、数据传输和逻辑处理层


SkyWalking探针层使用了GRPC作为数据传输框架,将采集的数据上报到SkyWalking服务端。


SkyWalking服务端接收数据后,利用各种插件来进行数据的分析和逻辑处理。比如:JVM相关插件,主要用于处理上报上来的JVM信息,数据库插件用来分析访问数据库的信息。然后在将数据存入到数据存储层。


1.4、数据存储层


SkyWalking的数据存储层支持多种主流数据库,可以自行到配置文件里查阅。我推荐使用ElasticSearch,存储量大,搜索性能又好。


1.5、数据展示层


SkyWalking 通过 Rocketbot 进行页面UI展示。可以在页面的左上角看到这个可爱的Rocketbot



2、搭建


知道了原理,搭建就很轻松了,使用SkyWalking其实就3个步骤:



  1. 搭建数据存储部件。

  2. 搭建SkyWalking服务端。

  3. 应用通过agent探针技术将数据采集上报给SkyWalking服务端。


2.1、搭建数据存储部件


SkyWalking支持多种存储方式,此处推荐采用Elasticsearch作为存储组件,存储的数据量较大,搜索响应快。


快速搭建Elasticsearch:



  1. 安装java:yum install java-1.8.0-openjdk-devel.x86_64

  2. 下载Elasticsearch安装包:http://www.elastic.co/cn/download…

  3. 修改elasticsearch.yml文件的部分字段:cluster.namenode.namepath.datapath.logsnetwork.hosthttp.portdiscovery.seed_hostscluster.initial_master_nodes。将字段的值改成对应的值。

  4. 在Elasticsearch的bin目录下执行./elasticsearch启动服务。

  5. 访问http://es-ip:9200,看到如下界面就代表安装成功。


{
"name": "node-1",
"cluster_name": "my-application",
"cluster_uuid": "GvK7v9HhS4qgCvfvU6lYCQ",
"version": {
"number": "7.17.1",
"build_flavor": "default",
"build_type": "rpm",
"build_hash": "e5acb99f822233d6ad4sdf44ce45a454xxxaasdfas323ab",
"build_date": "2023-02-23T22:20:54.153567231Z",
"build_snapshot": false,
"lucene_version": "8.11.1",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}

2.2、搭建SkyWalking服务端


搭建SkyWalking服务端只需要4步:


1、下载并解压skywalking:archive.apache.org/dist/skywal…



2、进入到安装目录下的修改配置文件:config/apllication.yaml。将存储修改为elasticsearch。



3、进入到安装目录下的bin目录,执行./startup.sh启动SkyWalking服务端。


4、此时使用jps命令,应该可以看到如下2个进程。一个是web页面进程,一个是接受和处理上报数据的进程。如果没有jps命令,那自行查看下是否配置了Java环境变量。 同时访问http://ip:8080应该可以看到如下界面。




2.3、应用采集上报数据


应用采集并且上报数据,直接使用agent探针方式。分为以下3步:


1、下载解压agentarchive.apache.org/dist/skywal…,找到skywalking-agent.jar



2、添加启动参数



  • 应用如果是jar命令启动,则直接添加启动参数即可:


java -javaagent:/自定义path/skywalking-agent.jar -Dskywalking.collector.backend_service={{agentUrl}} -jar xxxxxx.jar 

此处的{{agentUrl}}是SkyWalking服务端安装的地址,再加上11800端口。比如:10.20.0.55:11800




  • 应用如果是Docker镜像的部署方式,则需要将skywalking-agent.jar打到镜像里,类似下图:



3、启动项目后,即可看到监控数据,如下图:



3、UI页面使用


原理和搭建已经介绍完毕,接下来快速介绍UI页面的功能。下图标红的部分是重点关注区域:


3.1、仪表盘



  • APM:以全局(Global)、服务(Service)、服务实例(Instance)、端点(Endpoint)的维度展示各项指标。

  • Database:展示数据库的各项指标。




  • 服务(Service):某个微服务,或者某个应用。

  • 服务实例(Instance):某个微服务或者某个应用集群的一台实例或者一台负载。

  • 端点(Endpoint):某个Http请求的接口,或者 某个接口名+方法名。




3.2、拓扑图



3.3、追踪



关于UI界面的使用,还可以参考这个链接:juejin.cn/post/710630…,这里写的比较详细。


总结


本文主要从3个方面入手:原理、搭建、使用,介绍如何快速集成使用SkyWalking。核心重点:



  • SkyWalking其实就4部分组成:探针采集上报数据分析和逻辑处理、数据存储数据展示。安装使用简单、易上手。

  • 探针技术是SkyWalking的基石,说白了就是:在类加载时进行字节码转换增强,然后去拦截请求,采集上报数据。

  • UI页面的使用,多用用就熟悉了。


本篇完结!感谢你的阅读,欢迎点赞 关注 收藏 私信!!!


原文链接: http://www.mangod.top/articles/20…mp.weixin.qq.com/s/5P6vYSOCy…


作者:不焦躁的程序员
来源:juejin.cn/post/7288604780382879796
收起阅读 »

说出来你可能不信,分布式锁竟然这么简单...

大家好,我是小❤。 作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。 今天,让我们来聊聊分布式领域中那位不太引人注意却功不可没的角色,它就像是分布式系统的守卫,...
继续阅读 »

大家好,我是小❤。


作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。


今天,让我们来聊聊分布式领域中那位不太引人注意却功不可没的角色,它就像是分布式系统的守卫,保护着资源不被随意访问——这就是分布式锁!


想象一下,如果没有分布式锁,多个分布式节点同时涌入一个共享资源的访问时,就像一群饥肠辘辘的狼汇聚在一块肉前,谁都想咬一口,最后弄得肉丢了个精光,大家都吃不上。



而有了分布式锁,就像给这块肉上了道坚固的城墙,只有一只狼能够穿越,享受美味。


那它具体是怎么做的呢?这篇文章中,小❤将带大家一起了解分布式锁是如何解决分布式系统中的并发问题的。


什么是分布式锁?


在分布式系统中,分布式锁是一种机制,用于协调多个节点上的并发访问共享资源。


这个共享资源可以是数据库、文件、缓存或任何需要互斥访问的数据或资源。分布式锁确保了在任何给定时刻只有一个节点能够对资源进行操作,从而保持了数据的一致性和可靠性。


为什么要使用分布式锁?


1. 数据一致性


在分布式环境中,多个节点同时访问共享资源可能导致数据不一致的问题。分布式锁可以防止这种情况发生,确保数据的一致性。


2. 防止竞争条件


多个节点并发访问共享资源时可能出现竞争条件,这会导致不可预测的结果。分布式锁可以有效地防止竞争条件,确保操作按照预期顺序执行


3. 限制资源的访问


有些资源可能需要限制同时访问的数量,以避免过载或资源浪费。分布式锁可以帮助控制资源的访问


分布式锁要解决的问题


分布式锁的核心问题是如何在多个节点之间协调,以确保只有一个节点可以获得锁,而其他节点必须等待。



这涉及到以下关键问题:


1. 互斥性


只有一个节点能够获得锁,其他节点必须等待。这确保了资源的互斥访问。


2. 可重入性


指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。


说白了就是同一个线程再次进入同样代码时,可以再次拿到该锁。它的作用是:防止在同一线程中多次获取锁产生竞性条件而导致死锁发生


3. 超时释放


确保即使节点在业务过程中发生故障,锁也会被超时释放,既能防止不必要的线程等待和资源浪费,也能避免死锁。


分布式锁的实现方式


在分布式系统中,有多种方式可以实现分布式锁,就像是锁的品种不同,每种锁都有自己的特点。




  • 有基于数据库的锁,就像是厨师们用餐具把菜肴锁在柜子里,每个人都得排队去取。




  • 还有基于 ZooKeeper 的锁,它像是整个餐厅的门卫,只允许一个人进去,其他人只能在门口等。




  • 最后,还有基于缓存的锁,就像是一位服务员用号码牌帮你占座,先到先得。




1. 基于数据库的分布式锁


使用数据库表中的一行记录作为锁,通过事务来获取和释放锁。


例如,使用 MySQL 来实现事务锁。首先创建一张简单表,在某一个字段上创建唯一索引(保证多个请求新增字段时,只有一个请求可成功)。


CREATE TABLE `user` (  
  `id` bigint(20NOT NULL AUTO_INCREMENT,  
  `uname` varchar(255) DEFAULT NULL,  
  PRIMARY KEY (`id`),  
  UNIQUE KEY `name` (`uname`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4

当需要获取分布式锁时,执行以下语句:


INSERT INTO `user` (uname) VALUES ('unique_key')

由于 name 字段上加了唯一索引,所以当多个请求提交 insert 语句时,只有一个请求可成功。


使用 MySQL 实现分布式锁的优点是可靠性高,但性能较差,而且这把锁是非重入的,同一个线程在没有释放锁之前无法获得该锁


2. 基于ZooKeeper的分布式锁


Zookeeper(简称 zk)是一个为分布式应用提供一致性服务的中间组件,其内部是一个分层的文件系统目录树结构。


zk 规定其某一个目录下只能有唯一的一个文件名,其分布式锁的实现方式如下:



  1. 创建一个锁目录(ZNode) :首先,在 zk 中创建一个专门用于存储锁的目录,通常称为锁根节点。这个目录将包含所有获取锁的请求以及用于锁协调的节点。

  2. 获取锁:当一个节点想要获取锁时,它会在锁目录下创建一个临时顺序节点(Ephemeral Sequential Node)。zk 会为每个节点分配一个唯一的序列号,并根据序列号的大小来确定锁的获取顺序。

  3. 查看是否获得锁:节点在创建临时顺序节点后,需要检查自己的节点是否是锁目录中序列号最小的节点。如果是,表示节点获得了锁;如果不是,则节点需要监听比它序列号小的节点的删除事件。

  4. 监听锁释放:如果一个节点没有获得锁,它会设置一个监听器来监视比它序列号小的节点的删除事件。一旦前一个节点(序列号小的节点)释放了锁,zk 会通知等待的节点。

  5. 释放锁:当一个节点完成了对共享资源的操作后,它会删除自己创建的临时节点,这将触发 zk 通知等待的节点。


zk 分布式锁提供了良好的一致性和可用性,但部署和维护较为复杂,需要仔细处理各种边界情况,例如节点的创建、删除、网络分区等。


而且 zk 实现分布式锁的性能不太好,主要是获取和释放锁都需要在集群的 Leader 节点上执行,同步较慢。


3. 基于缓存的分布式锁


使用分布式缓存,如 Redis 或 Memcached,来存储锁信息,缓存方式性能较高,但需要处理分布式缓存的高可用性和一致性。


接下来,我们详细讨论一下在 Redis 中如何设计一个高可用的分布式锁以及可能会遇到的几个问题,包括:




  1. 死锁问题




  2. 锁提前释放




  3. 锁被其它线程误删




  4. 高可用问题




1)死锁问题


早期版本的 redis 没有 setnx 命令在写 key 时直接设置超时参数,需要用 expire 命令单独对锁设置过期时间,这可能会导致死锁问题。


比如,设置锁的过期时间执行失败了,导致后来的抢锁都会失败。


Lua脚本或SETNX


为了保证原子性,我们可以使用 Lua 脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用RedisSET 指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX],它也是原子性的。



SET key value [EX seconds] [PX milliseconds] [NX|XX]



  • NX:表示 key 不存在的时候,才能 set 成功,即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等待锁释放后,才能获取

  • EX seconds :设定 key 的过期时间,默认单位时间为秒

  • PX milliseconds: 设定 key 的过期时间,默认单位时间为毫秒

  • XX: 仅当 key 存在时设置值



在 Go 语言里面,关键代码如下所示:


func getLock() {    
   methodName := "getLock"    
   val, err := client.Do("set", methodName, "lock_value""nx""ex"100
   if err != nil {        
       zaplog.Errorf("%s set redis lock failed, %s", methodName, err)
       return
  }    
   if val == nil { 
       zaplog.Errorf("%s get redis lock failed", methodName)        
       return 
  }
   ... // 执行临界区代码,访问公共资源
   client.Del(lock.key()).Err() // 删除key,释放锁
}

2)锁提前释放


上述方案解决了加锁过期的原子性问题,不会产生死锁,但还是可能存在锁提前释放的问题。


如图所示,假设我们设置锁的过期时间为 5 秒,而业务执行需要 10 秒。



在线程 1 执行业务的过程中,它的锁被过期释放了,这时线程 2 是可以拿到锁的,也开始访问公共资源。


很明显,这种情况下导致了公共资源没有被严格串行访问,破坏了分布式锁的互斥性


这时,有爱动脑瓜子的小伙伴可能认为,既然加锁时间太短,那我们把锁的过期时间设置得长一些不就可以了吗?


其实不然,首先我们没法提前准确知道一个业务执行的具体时间。其次,公共资源的访问时间大概率是动态变化的,时间设置得过长也不好。


Redisson框架


所以,我们不妨给加锁线程一个自动续期的功能,即每隔一段时间检查锁是否还存在,如果存在就延长锁的时间,防止锁过期提前释放


这个功能需要用到守护线程,当前已经有开源框架帮我们解决了,它就是——Redisson,它的实现原理如图所示:



当线程 1 加锁成功后,就会启动一个 Watch dog 看门狗,它是一个后台线程,每隔 1 秒(可配置)检查业务是否还持有锁,以达到线程未主动释放锁,自动续期的效果。


3)锁被其它线程误删


除了锁提前释放,我们可能还会遇到锁被其它线程误删的问题。



如图所示,加锁线程 1 执行完业务后,去释放锁。但线程 1 自己的锁已经释放了,此时分布式锁是由线程 2 持有的,就会误删线程 2 的锁,但线程 2 的业务可能还没执行完毕,导致异常产生。


唯一 Value 值


要想解决锁被误删的问题,我们需要给每个线程的锁加一个唯一标识。


比如,在加锁时将 Value 设置为线程对应服务器的 IP。对应的 Go 语言关键代码如下:


const (  
   // HostIP,当前服务器的IP  
   HostIP = getLocalIP()
)

func getLock() {    
   methodName := "getLock"    
   val, err := client.Do("set", methodName, HostIP, "nx""ex"100
   if err != nil {        
       zaplog.Errorf("%s redis error, %s", methodName, err)
       return
  }    
   if val == nil { 
       zaplog.Errorf("%s get redis lock error", methodName)        
       return 
  }
   ... // 执行临界区代码,访问公共资源
   if client.Get(methodName) == HostIP {
       // 判断为当前服务器线程加的锁,才可以删除
       client.Del(lock.key()).Err()
  }
}

这样,在删除锁的时候判断一下 Value 是否为当前实例的 IP,就可以避免误删除其它线程锁的问题了。


为了保证严格的原子性,可以用 Lua 脚本代替以上代码,如下所示:


if redis.call('get',KEYS[1]) == ARGV[1] then
  return redis.call('del',KEYS[1])
else
  return 0
end;

4)Redlock高可用锁


前面几种方案都是基于单机版考虑,而实际业务中 Redis 一般都是集群部署的,所以我们接下来讨论一下 Redis 分布式锁的高可用问题。


试想一下,如果线程 1 在 Redis 的 master 主节点上拿到了锁,但是还没同步到 slave 从节点。


这时,如果主节点发生故障,从节点升级为主节点,其它线程就可以重新获取这个锁,此时可能有多个线程拿到同一个锁。即,分布式锁的互斥性遭到了破坏。


为了解决这个问题,Redis 的作者提出了专门支持分布式锁的算法:Redis Distributed Lock,简称 Redlock,其核心思想类似于注册中心的选举机制。



Redis 集群内部署多个 master 主节点,它们相互独立,即每个主节点之间不存在数据同步。


且节点数为单数个,每次当客户端抢锁时,需要从这几个 master 节点去申请锁,当从一半以上的节点上获取成功时,锁才算获取成功。


优缺点和常用实现方式


以上是业界常用的三种分布式锁实现方式,它们各自的优缺点如下:



  • 基于数据库的分布式锁:可靠性高,但性能较差,不适合高并发场景。

  • 基于ZooKeeper的分布式锁:提供良好的一致性和可用性,适合复杂的分布式场景,但部署和维护复杂,且性能比不上缓存的方式。

  • 基于缓存的分布式锁:性能较高,适合大部分场景,但需要处理缓存的高可用性。


其中,业界常用的分布式锁实现方式通常是基于缓存的方式,如使用 Redis 实现分布式锁。这是因为 Redis 性能优秀,而且可以满足大多数应用场景的需求。


小结


尽管分布式世界曲折离奇,但有了分布式锁,我们就像是看电影的观众,可以有条不紊地入场,分布式系统里的资源就像胶片一样,等待着我们一张一张地观赏。


这就是分布式的魅力!它或许令人又爱又恨,但正是科技世界的多样复杂性,才让我们的技术之旅变得更加精彩。



最后,希望这篇文章能够帮助大家更深入地理解分布式锁的重要性和实际应用。



想了解更多分布式相关的话题,可以看我另一篇文章,深入浅出:分布式、CAP和BASE理论



如果大家觉得有所收获或者启发,不妨动动小手关注我,然后把文章分享、点赞、加入在看哦~



xin猿意码


公众号


我是小❤,我们下期再见!


点个在看** 你最好看


作者:xin猿意码
来源:juejin.cn/post/7288166472131133474
收起阅读 »

如何将pdf的签章变成黑色脱密

前言 事情是这样的,前段时间同事接到一个需求,需要将项目系统的签章正文脱密下载。不经意间听到同事嘀咕找不到头绪,网上的相关资料也很少,于是帮忙研究研究。 实现的思路: 首先,我们必须要明白一个PDF中存在哪些东西?PDF可以存储各种类型的内容,包括文本、图片、...
继续阅读 »

前言


事情是这样的,前段时间同事接到一个需求,需要将项目系统的签章正文脱密下载。不经意间听到同事嘀咕找不到头绪,网上的相关资料也很少,于是帮忙研究研究。


实现的思路:


首先,我们必须要明白一个PDF中存在哪些东西?PDF可以存储各种类型的内容,包括文本、图片、图形、表格、注释、标记和多媒体元素。那么印章在我们的PDF中其实就是存储的一个图片,然后这个图片附加的有印章信息,可用于文件的有效性验证,说白了其实就是一种【特殊的图片】,那么我们需要做的就是如何找到这个图片并如何将这个图片变成黑色最后插入到pdf的原始位置。下面我们就分析一下其处理的过程。


准备工作


我们使用apache 提供的 pdfbox用来处理和操作。


<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.24</version>
</dependency>

过程分析


查找印章定义


印章定义通常存储在 PDF 的资源文件中,例如字体、图像等。因此,我们需要找到印章定义所对应的 PDAnnotation(签名列表)。不同厂商对 签名信息 的标识可能不同,因此我们需要查找 PDF 文件中的 PDAnnotation。在这一步中,我们需要使用一些调试技巧和定向猜测,通过debug的模式我们去找或者猜测一下厂商的印章签名是什么,比如金格的就是:GoldGrid:AddSeal 。这个签名就带了金格的厂商名。



  • 首先是加载文档:PDDocument document = PDDocument.load(new File("test.pdf"));



  • 其次是遍历文档,查找每一个页中是否含有印章签名信息


List<PDAnnotation> annotations = page.getAnnotations();
for (PDAnnotation annotation : annotations) {
if (KG_SIGN.equals(annotation.getSubtype()) || NTKO_SIGN.equals(annotation.getSubtype())) {
// todo
}
}

上诉步骤我们就完成了查询信息的全过程,接下来我们需要获取印章图片信息。


获取印章流


一旦我们找到了印章定义所对应的 PDAnnotation,我们就可以获取到印章图片信息中相关的附加信息,比如印章的位置信息,字体,文字等等信息。


PDRectangle rectangle = annotation.getRectangle();
float width = rectangle.getWidth();
float height = rectangle.getHeight();

上诉代码我们获取了印章图片的大小信息,用于后续我们填充印章时的文件信息。PDRectangle 对象定义了矩形区域的左下角坐标、宽度和高度等属性。


PDAppearanceDictionary appearanceDictionary = annotation.getAppearance();
PDAppearanceEntry normalAppearance = appearanceDictionary.getNormalAppearance();
PDAppearanceStream appearanceStream = normalAppearance.getAppearanceStream();
PDResources resources = appearanceStream.getResources();
PDImageXObject xObject = (PDImageXObject)resources.getXObject(xObjectName);

那么上面代码就是我们获取到的原始图片对象信息。通过对PDImageXObject进行操作以完成我们的目的。


PDResources 资源对象包含了注释所需的所有资源,例如字体、图像等。可以使用资源对象进行进一步的操作,例如替换资源、添加新资源等。


在PDF文件中,图像通常被保存为一个XObject对象,该对象包含了图像的信息,例如像素数据、颜色空间、压缩方式等。对于一个PDF文档中的图像对象,通常需要从资源(Resources)对象中获取。


处理原始图片


一旦我们找到了印章图片对象,我们需要将其变成黑色。印章通常是红色的,因此我们可以遍历图像的像素,并将红色像素点变成黑色像素点。在这一步中,我们需要使用一些图像处理技术,例如使用 Java 的 BufferedImage 类来访问和修改图像的像素。


public static void replaceRed2Black(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
// 获取图片的像素信息
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
// 循环遍历每一个像素点
for (int i = 0; i < pixels.length; i++) {
// 获取当前像素点的颜色
Color color = new Color(pixels[i]);
// 如果当前像素点的颜色为白色 rgb(255, 255, 255),颜色不变
if (color.getRed() == 255 && color.getGreen() == 255 && color.getBlue() == 255) {
pixels[i] &= 0x00FFFFFF;
}else{
// 其他颜色设置为黑色 :rgb(0, 0, 0)
pixels[i] &= 0xFF000000;
}
}
image.setRGB(0, 0, width, height, pixels, 0, width);
}

代码逻辑:首先获取图片的宽高信息,然后获取图片的像素信息,循环每一个像素,然后判断像素的颜色是什么色,如果不是白色那么就将颜色替换为黑色。


tips:这里其实有个小插曲,当时做的时候判断条件是如果为红色则将其变换为黑色,但是这里有个问题就是在红色边缘的时候,其颜色的rgb数字是一个区间,这样去替换的话,图片里面就会存在模糊和替换不全。所以后来灵光一现,改成现在这样。


插入处理后的图片


最后,我们需要将新的印章图像插入到 PDF 文件中原始印章的位置上,代码如下:


PDAppearanceStream newAppearanceStream = new PDAppearanceStream(appearanceStream.getCOSObject());
PDAppearanceContentStream newContentStream = new PDAppearanceContentStream(newAppearanceStream);
newContentStream.addRect(0, 0, width, height);
File file = new File("image.png");
PDImageXObject image = PDImageXObject.createFromFileByContent(file, document);
// 在内容流中绘制图片
newContentStream.drawImage(image, 0, 0, width, height);
// 关闭外观流对象和内容流对象
newContentStream.close();

这段代码是在Java语言中使用PDFBox库操作PDF文件时,创建一个新的外观流(Appearance Stream)对象,并在该流中绘制一张图片。


首先,通过调用PDAppearanceStream类的构造方法,创建一个新的外观流对象,并将其初始化为与原有外观流对象相同的COS对象。这里使用appearanceStream.getCOSObject()方法获取原有外观流对象的COS对象。然后,创建一个新的内容流(AppearanceContent Stream)对象,将其与新的外观流对象关联起来。


接下来,使用addRect()方法向内容流中添加一个矩形,其左下角坐标为(0,0),宽度为width,高度为height。该操作用于确定图片在外观流中的位置和大小。


然后,通过PDImageXObject类中的createFromFileByContent()方法创建一个PDImageXObject对象,该对象表示从文件中读取的图片。这里使用一个File对象和PDF文档对象document作为参数创建PDImageXObject对象。


接下来,使用drawImage()方法将读取的图片绘制到内容流中。该方法以PDImageXObject对象、x坐标、y坐标、宽度、高度作为参数,用于将指定的图片绘制到内容流中的指定位置。


最后,通过调用close()方法关闭内容流对象,从而生成一个完整的外观流对象。


到此我们就完成了印章的脱密下载的全过程,这个任务的难点在于怎么查找不同厂商对印章的签名定义以及对pdf的理解和工具API的理解。


作者:Aqoo
来源:juejin.cn/post/7221131955201687607
收起阅读 »

3个bug导致Kafka消息丢失,我人麻了

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug 发送MQ消息居然加了超时熔断 在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QP...
继续阅读 »

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug


发送MQ消息居然加了超时熔断


在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QPS超过20且失败率大于5%时触发熔断。这意味着当QPS=20时,只要有一条消息发送超时,整个系统就会熔断,无法继续发送MQ消息。
hystrix.command.default.circuitBreaker.errorThresholdPercentage=5


HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),
@HystrixProperty(name = "execution.timeout.enabled", value = "true")})
public void doSendMessage(Message message){
// 发送消息
}

之前系统一直运行正常,直到最近系统请求量上升才触发了这个bug。现在已经找不到是谁配置了这个过于激进的熔断策略了。真的非常气人!


一般情况下,发送MQ消息不会失败。但是在服务刚启动且未预热时,可能会有少量请求超过100毫秒,被Hystrix判断为失败。而恰好当时QPS超过了20,导致触发了熔断。


为什么发送MQ消息还需要加入熔断机制呢? 我很不理解啊


MQ(消息队列)本身就是用来削峰填谷的,可以支持非常高的并发量。无论是低峰期还是高峰期,只要给MQ发送端添加熔断机制都会导致数据严重不一致!我真的不太明白,为什么要在发送MQ消息时加入熔断机制。


另外,为什么要设定这么激进的熔断策略呢?仅有5%的失败率就导致服务100%不可用,这是哪个天才的逻辑呢?至少在失败率超过30%且QPS超过200的情况下,才需要考虑使用熔断机制吧。在QPS为20的情况下,即使100%的请求都失败了,也不会拖垮应用服务,更何况只是区区5%的失败率呢。


这是典型的为了熔断而熔断!把熔断变成政治正确的事情。不加熔断反而变成异类,会被人瞧不起!


吞掉了异常


虽然添加熔断策略,会导致发送MQ失败抛出熔断异常,但是上层代码考虑了消息发送失败的情况。流程中包含分布式重试方案,但是排查问题时我才发现,重试策略居然没有生效!这是什么原因?


在一番排查后我发现,发送MQ的代码 吞掉了异常信息,没有向上抛出!


去掉无用的业务逻辑后,我把代码粘贴到下面。


try{
doSendMessage(msg);
}catch(Exception e){
log.error("发送MQ异常:{}", msg, e);
//发送失败MQ消息到公司故障群!
}

消息发送异常后,仅仅在系统打印了ERROR日志,并将失败消息发送到了公司的IM群里。然而,这样的处理方式根本无法让上层方法意识到消息发送失败的情况,更别提察觉到由于熔断而导致的发送失败了。在熔断场景下,消息根本没有被发送给MQ,而是直接失败。因此,可以确定消息一定丢失了。


面试时我们经常会被问到”如何保证消息不丢“。大家能够滔滔不绝地说出七八个策略来确保消息的可靠性。然而当写起代码时,为什么会犯下如此低级的错误呢?


仅仅打印ERROR日志就能解决问题吗?将故障消息上报到公司的群里就有人关注吗?考虑到公司每天各种群里都会涌现成千上万条消息,谁能保证一定有人会关注到!国庆节放假八天,会有人关注公司故障群的消息吗?


很多人在处理异常时习惯性的吞掉异常,害怕把异常抛给上游处理。系统应该处理Rpc调用失败、MQ发送失败的场景,不应该吞掉异常,而是应该重试!一般流程都会有整体的分布式重试机制,出问题不怕、出异常也不怕,只要把问题抛出,由上游发起重试即可。


悄咪咪的把异常吞掉,不是处理问题的办法!


于是我只能从日志中心捞日志,然后把消息手动发送到MQ中。我真的想问,这代码是人写的吗?


服务关闭期间,生产者先于消费者关闭,导致消息发送失败


出问题的系统流程是 先消费TopicA ,然后发送消息到Topic B。但是服务实例关闭期间,发送TopicB消息时,报错 producer has closed。为什么消费者还未关闭,生产者先关闭呢?


这个问题属于服务优雅发布范畴,一般情况下都应该首先关闭消费者,切断系统流量入口,然后再关闭生产者实例。


经过排查,发现问题的原因是生产者实例注册了shutdown hook钩子程序。也就是说,只要进程收到Kill信息,生产者就会启动关闭流程。这解释了为什么会出现这个问题。


针对这个问题,我修改了策略,删除了生产者注册shutdown hook钩子的逻辑。确保消费者先关闭!生产者后关闭。


总结


如果有人问我:消息发送失败的可能原因,我是肯定想不到会有这三个原因的。也是涨见识了。


很多人滔滔不绝的谈着 消息不丢不重,背后写的代码却让人不忍直视!


作者:他是程序员
来源:juejin.cn/post/7288228582692929547
收起阅读 »

听说你会架构设计?来,弄一个公交&地铁乘车系统

1. 引言 1.1 上班通勤的日常 “叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。 突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。 这个时候,通勤的老难题又摆...
继续阅读 »

1. 引言


1.1 上班通勤的日常


“叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。



突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。


这个时候,通勤的老难题又摆在了你面前:要不要吃完这口面包、刷牙和洗脸,还是先冲出门赶车?


好不容易做出了一个艰难的决定——放下面包,快步冲出门。你拿出手机,点开了熟悉的地铁乘车 App 或公交地铁乘车码小程序。


然后,一张二维码在屏幕上亮了起来,这可是你每天通勤的“敲门砖”。





你快步走到地铁站,将手机二维码扫描在闸机上,"嗖"的一声,闸机打开,你轻松通过,不再需要排队买票,不再被早高峰的拥挤闹心。


你走进地铁车厢,挤到了一个角落,拿出手机,开始计划一天的工作。


1.2 公交&地铁乘车系统


正如上文所说,人们只需要一台手机,一个二维码就可以完成上班通勤的所有事项。


那这个便捷的公交或地铁乘车系统是如何设计的呢?它背后的技术和架构是怎样支撑着你我每天的通勤生活呢?


今天让我们一起揭开这个现代都市打工人通勤小能手的面纱,深入探讨乘车系统的设计与实现


在这个文章中,小❤将带你走进乘车系统的世界,一探究竟,看看它是如何在短短几年内从科幻电影中走出来,成为我们日常生活不可或缺的一部分。


2. 需求设计


2.1 功能需求





  • 用户注册和登录: 用户可以通过手机应用或小程序注册账号,并使用账号登录系统。




  • 路线查询: 用户可以查询地铁的线路和站点信息,包括发车时间、车票价格等。




  • 获取乘车二维码: 系统根据用户的信息生成乘车二维码。




  • 获取地铁实时位置: 用户可以查询地铁的实时位置,并查看地铁离当前站台还有多久到达。




  • 乘车扫描和自动支付: 用户在入站和出站时通过扫描二维码来完成乘车,系统根据乘车里程自动计算费用并进行支付。




  • 交易记录查询: 用户可以查询自己的交易历史记录,包括乘车时间、金额、线路等信息。




2.2 乘车系统的非功能需求


乘车系统的用户量非常大,据《中国主要城市通勤检测报告-2023》数据显示,一线城市每天乘公交&地铁上班的的人数普遍超过千万,平均通勤时间在 45-60 分钟,并集中在早高峰和晚高峰时段。


所以,设计一个热点数据分布非均匀、人群分布非均匀的乘车系统时,需要考虑如下几点:




  • 用户分布不均匀,一线城市的乘车系统用户,超出普通城市几个数量级。




  • 时间分布不均匀,乘车系统的设计初衷是方便上下班通勤,所以早晚高峰的用户数会高出其它时间段几个数量级。




  • 高并发: 考虑到公交车/地铁系统可能同时有大量的用户在高峰时段使用,系统需要具备高并发处理能力。




  • 高性能: 为了提供快速的查询和支付服务,系统需要具备高性能,响应时间应尽可能短。




  • 可扩展性: 随着用户数量的增加,系统应该容易扩展,以满足未来的需求。




  • 可用性: 系统需要保证24/7的可用性,随时提供服务。




  • 安全和隐私保护: 系统需要确保用户数据的安全和隐私,包括支付信息和个人信息的保护。




3. 概要设计


3.1 核心组件





  • 前端应用: 开发手机 App 和小程序,提供用户注册、登录、查询等功能。




  • 后端服务: 设计后端服务,包括用户管理、路线查询、二维码管理、订单处理、支付系统等。




  • 数据库: 使用关系型数据库 MySQL 集群存储用户信息、路线信息、交易记录等数据。




  • 推送系统: 将乘车后的支付结果,通过在线和离线两种方式推送给用户手机上。




  • 负载均衡和消息队列: 考虑使用负载均衡和消息队列技术来提高系统性能。




3.2 乘车流程


1)用户手机与后台系统的交互


交互时序图如下:



1. 用户注册和登录: 用户首先需要在手机应用上注册并登录系统,提供个人信息,包括用户名、手机号码、支付方式等。


2. 查询乘车信息: 用户可以使用手机应用查询公交车/地铁的路线和票价信息,用户可以根据自己的出行需求选择合适的线路。


3. 生成乘车二维码: 用户登录后,系统会生成一个用于乘车的二维码,这个二维码可以在用户手机上随时查看。这个二维码是城市公交系统的通用乘车二维码,同时该码关联到用户的账户和付款方式,用户可以随时使用它乘坐任何一辆公交车或地铁。


2)用户手机与公交车的交互


交互 UML 状态图如下:





  1. 用户进站扫码: 当用户进入地铁站时,他们将手机上的乘车码扫描在进站设备上。这个设备将扫描到的乘车码发送给后台系统。




  2. 进站数据处理: 后台系统接收到进站信息后,会验证乘车码的有效性,检查用户是否有进站记录,并记录下进站的时间和地点。




  3. 用户出站扫码: 用户在乘车结束后,将手机上的乘车码扫描在出站设备上。




  4. 出站数据处理: 后台系统接收到出站信息后,会验证乘车码的有效性,检查用户是否有对应的进站记录,并记录下出站的时间和地点。




3)后台系统的处理




  1. 乘车费用计算: 基于用户的进站和出站地点以及乘车规则,后台系统计算乘车费用。这个费用可以根据不同的城市和运营商有所不同。




  2. 费用记录和扣款: 系统记录下乘车费用,并从用户的付款方式(例如,支付宝或微信钱包)中扣除费用。




  3. 乘车记录存储: 所有的乘车记录,包括进站、出站、费用等信息,被存储在乘车记录表中,以便用户查看和服务提供商进行结算。




  4. 通知用户: 如果有需要,系统可以向用户发送通知,告知他们的乘车费用已被扣除。




  5. 数据库交互: 在整个过程中,系统需要与数据库交互来存储和检索用户信息、乘车记录、费用信息等数据。




3. 详细设计


3.1 数据库设计



  • 用户信息表(User) ,包括用户ID、手机号、密码、支付方式、创建时间等。

  • 二维码表 (QRCode) ,包括二维码ID、用户ID、城市ID、生成时间、有效期及二维码数据等。

  • 车辆&地铁车次表 (Vehicle) ,包括车辆ID、车牌或地铁列车号、车型(公交、地铁)、扫描设备序列号等。

  • 乘车记录表 (TripRecord) ,包括记录ID、用户ID、车辆ID、上下车时间、起止站点等。

  • 支付记录表 (PaymentRecord) ,包括支付ID、乘车记录ID、交易时间、交易金额、支付方式、支付状态等。


以上是一些在公交车&地铁乘车系统中需要设计的数据库表及其字段的基本信息,后续可根据具体需求和系统规模,还可以进一步优化表结构和字段设计,以满足性能和扩展性要求。


详细设计除了要设计出表结构以外,我们还针对两个核心问题进行讨论:



  • 最短路线查询




  • 乘车二维码管理




3.2 最短路线查询


根据交通部门给的公交&地铁路线,我们可以绘制如下站点图:



假设图中的站点有 A-F,涉及到的交通工具有地铁 1 号线和 2 路公交,用户的起点和终点分别为 A、F 点。我们可以使用 Dijkstra 算法来求两点之间的最短路径,具体步骤为:


步骤已遍历集合未遍历集合
1选入A,此时最短路径 A->A = 0,再以 A 为中间点,开始寻找下一个邻近节点{B、C、D、E、F},其中与 A 相邻的节点有 B 和 C,AB=6,AC=3。接下来,选取较短的路径节点 C 开始遍历
2选取C,A->C=3,此时已遍历集合为{A、C},以 A 和 C 为中间点,开始寻找下一个邻近节点{B、D、E、F},其中与 A、C 相邻的节点有 B 和 D,AB=6,ACD=3+4=7。接下来,选取较短的路径节点 B 开始遍历
3选取B,A->B=6,此时已遍历集合为{A、C、B},A 相邻的节点已经遍历结束,开始寻找和 B、C 相近的节点{D、E、F},其中与 B、C 相邻的节点有 D,节点 D 在之前已经有了一个距离记录(7),现在新的可选路径是 ABD=6+5=11。显然第一个路径更短,于是将 D 的最近距离 7 加入到集合中
4选取D,A->D=7,此时已遍历集合为{A、C、B、D},寻找 D 相邻的节点{E、F},其中 DE=2,DF=3,选取最近路径的节点 E 加入集合
5选取 E,A->E=7+2=9,此时已遍历集合为{A、C、B、D、E},继续寻找 D 和 E 相近的节点{F},其中 DF=3,DEF=2+5=7,于是F的最近距离为7+3=10.
6选取F,A->F=10,此时遍历集合为{A、C、B、D、E、F}所有节点已遍历结束,从 A 点出发,它们的最近距离分别为{A=0,C=3,B=6,D=7,E=9,F=10}

在用户查询路线之前,交通部门会把公交 & 地铁的站点经纬度信息输入到路线管理系统,并根据二维的空间经纬度编码存储对应的站点信息。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成 4 个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识用户或站点的位置信息。再通过 Redis 的 GeoHash 算法,来获取用户出发点附近的所有站点信息。


GeoHash 算法的原理是将一个位置的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有站点


一旦获得了起始地点的经纬度,系统就可以根据附近的站点信息,调用路线管理系统来查找最佳的公交或地铁路线。


一旦用户选择了一条路线,导航引擎启动并提供实时导航指引。导航引擎可能会使用地图数据和 GPS 定位来指导用户前往起止站点。


3.3 乘车二维码管理


乘车码是通过 QR 码(Quick Response Code)技术生成的,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型,如图所示:



二维码的生成非常简单,拿 Go 语言来举例,只需引入一个三方库:


import "github.com/skip2/go-qrcode"

func main() {
    qr,err:=qrcode.New("https://mp.weixin.qq.com",qrcode.Medium)
if err != nil {
    log.Fatal(err)
else {
    qr.BackgroundColor = color.RGBA{50,205,50,255//定义背景色
    qr.ForegroundColor = color.White //定义前景色
    qr.WriteFile(256,"./wechatgzh_qrcode.png"//转成图片保存
    }
}

以下是该功能用户和系统之间的交互、二维码信息存储、以及高并发请求处理的详细说明:



  1. 用户与系统交互: 用户首先在手机 App 上登录,系统会验证用户的身份和付款方式。一旦验证成功,系统根据用户的身份信息和付款方式,动态生成一个 QR 码,这个 QR 码包含了用户的标识信息和相关的乘车参数。

  2. 二维码信息存储: 生成的二维码信息需要在后台进行存储和关联。通常,这些信息会存储在一个专门的数据库表中,该表包含以下字段:



    • 二维码ID:主键ID,唯一标识一个二维码。

    • 用户ID:与乘车码关联的用户唯一标识。

    • 二维码数据:QR码的内容,包括用户信息和乘车参数。

    • 生成时间:二维码生成的时间戳,用于后续的验证和管理。

    • 有效期限:二维码的有效期,通常会设置一个时间限制,以保证安全性。



  3. 高并发请求处理: 在高并发情况下,大量的用户会同时生成和扫描二维码,因此需要一些策略来处理这些请求:



    • 负载均衡: 后台系统可以采用负载均衡技术,将请求分散到多个服务器上,以分担服务器的负载。

    • 缓存优化: 二维码的生成是相对耗时的操作,可以采用 Redis 来缓存已生成的二维码,避免重复生成。

    • 限制频率: 为了防止滥用,可以限制每个用户生成二维码的频率,例如,每分钟只允许生成 5  次,这可以通过限流的方式来实现。




总之,通过 QR 码技术生成乘车码,后台系统需要具备高并发处理的能力,包括负载均衡、缓存和频率限制等策略,以确保用户能够快速获得有效的乘车二维码。


同时,二维码信息需要被安全地存储和管理,比如:加密存储以保护用户的隐私和付款信息。



不清楚如何限流的,可以看我之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 乘车系统的发展


4.1 其它设计


除此之外,公交车或地铁的定位和到站时间计算可能还涉及定位设备、GPS 系统、NoSQL 数据库、用户 TCP 连接管理系统等核心组件,并通过实时数据采集、位置处理、到站时间计算和信息推送等流程来为用户提供准确的乘车信息。


同时,自动支付也是为了方便用户的重要功能,可以通过与第三方支付平台的集成来实现。


4.2 未来发展


公交车/地铁乘车系统的未来发展可以包括以下方向:



  • 智能化乘车: 引入智能设备,如人脸自动识别乘客、人脸扣款等。

  • 大数据分析: 利用大数据技术分析乘车数据,提供更好的服务。


在设计和发展过程中,也要不断考虑用户体验、性能和安全,确保系统能够满足不断增长的需求。


由于篇幅有限,文章就到此结束了。


希望读者们能对公交&地铁乘车系统的设计有更深入的了解,并和小❤一起期待未来更多的交通创新解决方案叭~


作者:xin猿意码
来源:juejin.cn/post/7287495466514055202
收起阅读 »

喝了100杯酱香拿铁,我顿悟了锁的精髓

大家好,我是哪吒。 上一篇提到了锁粒度的问题,使用“越细粒度的锁越好”,真的是这样吗?会不会产生一些其它问题? 先说结论,可能会产生死锁问题。 下面还是以购买酱香拿铁为例: 1、定义咖啡实体类Coffee @Data public class Coffee ...
继续阅读 »

大家好,我是哪吒。


上一篇提到了锁粒度的问题,使用“越细粒度的锁越好”,真的是这样吗?会不会产生一些其它问题?


先说结论,可能会产生死锁问题。


下面还是以购买酱香拿铁为例:



1、定义咖啡实体类Coffee


@Data
public class Coffee {
// 酱香拿铁
private String name;

// 库存
public Integer inventory;

public ReentrantLock lock = new ReentrantLock();
}

2、初始化数据


private static List<Coffee> coffeeList = generateCoffee();

public static List<Coffee> generateCoffee(){
List<Coffee> coffeeList = new ArrayList<>();
coffeeList.add(new Coffee("酱香拿铁1", 100));
coffeeList.add(new Coffee("酱香拿铁2", 100));
coffeeList.add(new Coffee("酱香拿铁3", 100));
coffeeList.add(new Coffee("酱香拿铁4", 100));
coffeeList.add(new Coffee("酱香拿铁5", 100));
return coffeeList;
}

3、随机获取n杯咖啡


// 随机获取n杯咖啡
private static List<Coffee> getCoffees(int n) {
if(n >= coffeeList.size()){
return coffeeList;
}

List<Coffee> randomList = Stream.iterate(RandomUtils.nextInt(n), i -> RandomUtils.nextInt(coffeeList.size()))
.distinct()// 去重
.map(coffeeList::get)// 跟据上面取得的下标获取咖啡
.limit(n)// 截取前面 需要随机获取的咖啡
.collect(Collectors.toList());
return randomList;
}

4、购买咖啡


private static boolean buyCoffees(List<Coffee> coffees) {
//存放所有获得的锁
List<ReentrantLock> locks = new ArrayList<>();
for (Coffee coffee : coffees) {
try {
// 获得锁3秒超时
if (coffee.lock.tryLock(3, TimeUnit.SECONDS)) {
// 拿到锁之后,扣减咖啡库存
locks.add(coffee.lock);
coffeeList = coffeeList.stream().map(x -> {
// 购买了哪个,就减哪个
if (coffee.getName().equals(x.getName())) {
x.inventory--;
}
return x;
}).collect(Collectors.toList());
} else {
locks.forEach(ReentrantLock::unlock);
return false;
}
} catch (InterruptedException e) {
}
}
locks.forEach(ReentrantLock::unlock);
return true;
}

3、通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数


public static void main(String[] args){
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Coffee> getCoffees = getCoffees(2);
//Collections.sort(getCoffees, Comparator.comparing(Coffee::getName));
return buyCoffees(getCoffees);
})
.filter(result -> result)
.count();

stopWatch.stop();
System.out.println("成功次数:"+success);
System.out.println("方法耗时:"+stopWatch.getTotalTimeSeconds()+"秒");
for (Coffee coffee : coffeeList) {
System.out.println(coffee.getName()+"-剩余:"+coffee.getInventory()+"杯");
}
}


耗时有点久啊,20多秒。


数据对不对?



  • 酱香拿铁1卖了53杯;

  • 酱香拿铁2卖了57杯;

  • 酱香拿铁3卖了20杯;

  • 酱香拿铁4卖了22杯;

  • 酱香拿铁5卖了19杯;

  • 一共卖了171杯。


数量也对不上,应该卖掉200杯才对,哪里出问题了?


4、使用visualvm测一下:


果不其然,出问题了,产生了死锁。


线程 m 在等待的一个锁被线程 n 持有,线程 n 在等待的另一把锁被线程 m 持有。



  1. 比如美杜莎买了酱香拿铁1和酱香拿铁2,小医仙买了酱香拿铁2和酱香拿铁1;

  2. 美杜莎先获得了酱香拿铁1的锁,小医仙获得了酱香拿铁2的锁;

  3. 然后美杜莎和小医仙接下来要分别获取 酱香拿铁2 和 酱香拿铁1 的锁;

  4. 这个时候锁已经被对方获取了,只能相互等待一直到 3 秒超时。



5、如何解决呢?


让大家都先拿一样的酱香拿铁不就好了。让所有线程都先获取酱香拿铁1的锁,然后再获取酱香拿铁2的锁,这样就不会出问题了。


也就是在随机获取n杯咖啡后,对其进行排序即可。


// 通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Coffee> getCoffees = getCoffees(2);
// 根据咖啡名称进行排序
Collections.sort(getCoffees, Comparator.comparing(Coffee::getName));
return buyCoffees(getCoffees);
})
.filter(result -> result)
.count();

6、再测试一下



  • 成功次数100;

  • 咖啡卖掉了200杯,数量也对得上。

  • 代码执行速度也得到了质的飞跃,因为不用没有循环等待锁的时间了。



看来真的不是越细粒度的锁越好,真的会产生死锁问题。通过对酱香拿铁进行排序,解决了死锁问题,避免循环等待,效率也得到了提升。


作者:哪吒编程
来源:juejin.cn/post/7287429638020005944
收起阅读 »

提升接口性能的39个方法,两万字总结,太全了!

为了更好评估后端接口性能,我们需要对不同行为的耗时进行比较。从上图可以看出,一个CPU周期少于1纳秒,而一次从北京到上海的跨地域访问可能需要约30毫秒。怎么计算跨地域耗时呢? 我们已知光在真空中传播,折射率为 1,其光速约为 c=30 万公里/秒,当光在其他...
继续阅读 »

image.png


为了更好评估后端接口性能,我们需要对不同行为的耗时进行比较。从上图可以看出,一个CPU周期少于1纳秒,而一次从北京到上海的跨地域访问可能需要约30毫秒。怎么计算跨地域耗时呢?



我们已知光在真空中传播,折射率为 1,其光速约为 c=30 万公里/秒,当光在其他介质里来面传播,其介质折射自率为 n,光在其中的速度就降为 v=c/n,光纤的材料是二氧化硅,其折射率 n 为 1.44 左右,计算延迟的时候,可以近似认为 1.5,我们通过计算可以得出光纤中的光传输速度近似为 v=c/1.5= 20 万公里/秒。




以北京和深圳为例,直线距离 1920 公里,接近 2000 公里,传输介质如果使用光纤光缆,那么延迟时间 t=L/v = 0.2 万公里/20 万公里/秒=10ms ,也就是说从北京到深圳拉一根 2000 公里的光缆,单纯的距离延迟就要 10ms ,实际上是没有这么长的光缆的,中间是需要通过基站来进行中继,并且当光功率损耗到一定值以后,需要通过转换器加强功率以后继续传输,这个中转也是要消耗时间的。另外数据包在网络中长距离传输的时候是会经过多次的封包和拆包,这个也会消耗时间。




综合考虑各种情况以后,以北京到深圳为例,总的公网延迟大约在 40ms 左右,北京到上海的公网延迟大约在 30ms,如果数据出国的话,延迟会更大,比如中国到美国,延迟一般在 150ms ~ 200ms 左右,因为要经过太平洋的海底光缆过去的。



如果让你进行后端接口的优化,你是首选优化代码行数?还是首选避免跨地域访问呢?


在评估接口性能时,我们需要首先找出最耗时的部分,并优化它,这样优化效果才会立竿见影。上图提供了一个很好的参考。


需要注意的是,上图中没有显示机房内网络的耗时。一次机房内网络的延迟(Ping)通常在1毫秒以内,相比跨地域网络延迟要少很多。


对于机房内的访问,Redis缓存的访问耗时通常在1-5毫秒之间,而数据库的主键索引访问耗时在5-15毫秒之间。当然,这两者最大的区别不仅仅在于耗时,而更重要的是它们在承受高并发访问方面的能力。Redis单机可以承受10万并发(往往瓶颈在网络带宽和CPU),而MySQL要考虑主从读写分离和分库分表,才能稳定支持5千并发以上的访问。


1. 优化前端接口


1.1 核心数据和非核心数据拆分为多个接口


我曾经对用户(会员)主页接口进行了优化,该接口返回的数据非常庞大。由于各个模块的数据都在同一个接口中,只要其中一部分数据的查询耗时较长,整体性能就会下降,导致接口的失败率增加,前端无法展示核心数据。这主要是因为核心数据和非核心数据没有进行隔离,耗时数据和非耗时数据没有分开。


对于庞大的接口,我们需要先梳理每个模块中数据的获取逻辑和性能情况,明确前端必须展示和重点关注的核心数据,并确保这些数据能够快速、稳定地响应给前端。而非核心的数据和性能较差的数据则可以拆分到另外的接口中,即使这些接口的失败率较高,对用户影响也不大。


这种优化方式除了能保证快速返回核心数据,也能提高稳定性。如果非核心数据故障,可以单独降级,不会影响核心数据展示,大大提高了稳定性。


1.2 前端并行调用多个接口


后端提供给前端的接口应保证能够独立调用,避免出现需要先调用A接口再调用B接口的情况。如果接口设计不合理,前端需要的总耗时将是A接口耗时与B接口耗时之和。相反,如果接口能够独立调用,总耗时将取决于A接口和B接口中耗时较长的那个。显然,后者的性能更优。


在A接口与B接口都依赖相同的公共数据的情况下,会导致重复查询。为了优化总耗时,重复查询是无法避免的,因此应着重优化公共数据的性能。


在代码设计层面,应封装每个模块的取值逻辑,避免A接口与B接口出现重复代码或拷贝代码的情况。


1.3 使用MD5加密,防篡改数据,减少重复校验


在提单接口中,需要校验用户对应商品的可见性、是否符合优惠活动规则以及是否可用对应的优惠券等内容。由于用户可能篡改报文来伪造提单请求,后端必须进行校验。然而,由于提单链路本身耗时较长,多次校验以上数据将大大增加接口的耗时。那么,是否可以不进行以上内容的校验呢?


是可以的。在用户提单页面,商品数据、优惠活动数据以及优惠券等数据都是预览接口校验过的。后端可以生成一个预览Token,并将预览结果存在缓存中,前端在提单接口中指定预览Token。后端将校验提单数据和预览数据是否一致,如果不一致,则说明用户伪造了请求。


为了避免预览数据占用过多的缓存空间,可以设置一个过期时间,例如预览数据在15分钟内不进行下单操作,则会自动失效。另外,还可以对关键数据进行MD5加密处理,加密后的数据只有64位,数据量大大减少。后端在提单接口中对关键数据进行MD5加密,并与缓存中的MD5值进行比对,如果不一致,则说明用户伪造了提单数据。


更详细请参考# 如何防止提单数据被篡改?


1.4 同步写接口改为异步写接口


在写接口耗时较高的情况下,可以采取将接口拆分为两步来优化性能。首先,第一步是接收请求并创建一个异步任务,然后将任务交给后端进行处理。第二步是前端轮训异步任务的执行结果,以获取最终结果。


通过将同步接口异步化,可以避免后端线程资源被长时间占用,并且可以避免浏览器和服务器的socket连接被长时间占用,从而提高系统的并发能力和稳定性。


此外,还可以在前端接口设置更长的轮训时间,以有效提高接口的成功率,降低同步接口超时失败的概率,提升系统的性能和用户体验。


1.5 页面静态化


在电商领域,商品详情页和活动详情页通常会有非常高的流量,特别是在秒杀场景或大促场景下,流量会更高。同时,商品详情页通常包含大量的信息,例如商品介绍、商品参数等,导致每次访问商品详情都需要访问后端接口,给后端接口带来很大的压力。


为了解决这个问题,可以考虑将商品详情页中不会变动的部分(如商品介绍、头图、商品参数等)静态化到html文件中,前端浏览器直接访问这些静态文件,而无需访问后端接口。这样做可以极大地减轻商品详情接口的查询压力。


然而,对于未上架的商品详情页、后台管理等页面,仍然需要查询商品详情接口来获取最新的信息。


页面静态化需要先使用模版工具例如Thymeleaf等,将商品详情数据渲染到Html文件,然后使用运维工具(rsync)将html文件同步到各个nginx机器。前端就可以访问对应的商品详情页。


当商品上下架状态变化时,将对应Html文件重新覆盖或置为失效。


1.6 不变资源访问CDN



CDN(内容分发网络)是一种分布式网络架构,它将网站的静态内容缓存在全球各地的服务器上,使用户能够从最近的服务器获取所需内容,从而加速用户访问。这样,用户不需要从原始服务器请求内容,可以减少因网络延迟导致的等待时间,提高用户的访问速度和体验。



通过注入静态Html文件到CDN,可以避免每次用户的请求都访问原始服务器。相反,这些文件会被缓存在CDN的服务器上,因此用户可以直接从离他们最近的服务器获取内容。这种方式可以大大减少因网络延迟导致的潜在用户流失,因为用户能够更快地获取所需的信息。


此外,CDN的使用还可以提高系统在高并发场景下的稳定性。在高并发情况下,原始服务器可能无法承受大量的请求流量,并可能导致系统崩溃或响应变慢。但是,通过将静态Html文件注入到CDN,让CDN来处理部分请求,分担了原始服务器的负载,从而提高了整个系统的稳定性。


通过将商品详情、活动详情等静态Html文件注入到CDN,可以加速用户访问速度,减少用户因网络延迟而流失的可能性,并提高系统在高并发场景下的稳定性。


2. 调用链路优化


调用链路优化重点减少RPC的调用、减少跨地域调用。


2.1 减少跨地域调用


刚才我提到了北京到上海的跨地域调用需要耗费大约30毫秒的时间,这个耗时是相当高的,所以我们应该特别关注调用链路上是否存在跨地域调用的情况。这些跨地域调用包括Rpc调用、Http调用、数据库调用、缓存调用以及MQ调用等等。在整理调用链路的时候,我们还应该标注出跨地域调用的次数,例如跨地域调用数据库可能会出现多次,在链路上我们需要明确标记。我们可以考虑通过降低调用次数来提高性能,因此在设计优化方案时,我们应该特别关注如何减少跨地域调用的次数。


举个例子,在某种情况下,假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。


2.2 单元化架构:不同的用户路由到不同的集群单元


如果主数据库位于北京,那么南方的用户每次写请求就只能通过跨地域访问来完成吗?实际上并非如此。数据库的主库不仅可以存在于一个地域,而是可以在多个地域上部署主数据库。将每个用户归属于最近的地域,该用户的请求都会被路由到所在地域的数据库。这样的部署不仅提升了系统性能,还提高了系统的容灾等级,即使单个机房发生故障也不会影响全网的用户。


这个思想类似于CDN(内容分发网络),它能够将用户请求路由到最近的节点。事实上,由于用户的存储数据已经在该地域的数据库中,用户的请求极少需要切换到其他地域。


为了实现这一点,我们需要一个用户路由服务来提供用户所在地域的查询,并且能够提供高并发的访问。


除了数据库之外,其他的存储中间件(如MQ、Redis等)以及Rpc框架都需要具备单元化架构能力。


当我们无法避免跨地域调用时,我们可以选择整体上跨地域调用次数最少的方案来进行优化。


2.3 微服务拆分过细会导致Rpc调用较多


微服务拆分过细会导致更多的RPC调用,一次简单的请求可能就涉及四五个服务,当访问量非常高时,多出来的三五次Rpc调用会导致接口耗时增加很多。


每个服务都需要处理网络IO,序列化反序列化,服务的GC 也会导致耗时增加,这样算下来一个大服务的性能往往优于5个微服务。


当然服务过于臃肿会降低开发维护效率,也不利于技术升级。微服务过多也有问题,例如增加整体链路耗时、基础架构升级工作量变大、单个需求代码变更的服务更多等弊端。需要你权衡开发效率、线上性能、领域划分等多方面因素。


总之应该极力避免微服务过多的情况。


怎么评估微服务过多呢?我的个人经验是:团队内平均一个人两个服务以上,就是微服务过多了。例如三个人的团队6个服务,5个人的团队10个服务。


2.4 去掉中间商,减少Rpc调用


当整个系统的调用链路中涉及到过多的Rpc调用时,可以通过去除中间服务的方式减少Rpc调用。例如从A服务到E服务的调用链路包含了4次Rpc调用(A->B->C->D->E),而我们可以评估中间的B、C、D三个服务的功能是否冗余,是否只是作为转发服务而没有太多的业务逻辑,如果是的话,我们可以考虑让A服务直接调用E服务,从而避免中间的Rpc调用,减少系统的负担。


总的来说,无论是调用链路过长或是微服务过多,都可能导致过多的Rpc请求,因此可以尝试去除中间的服务来优化系统性能。


2.5 提供Client工具方法处理,而非Rpc调用


如果中间服务有业务逻辑,不能直接移除,可以考虑使用基于Java Client工具方法的服务提供方式,而非Rpc方式。


举例来说,如果存在一个调用链路为A->B->C,其中B服务有自己的业务逻辑。此时B服务可以考虑提供一个Java Client jar包给A服务使用。B服务所依赖的数据可以由A服务提供,这样就减少1次 A 服务到B 服务的Rpc调用。


这样做有一个好处,当A、B都共同依赖相同的数据,A服务查询一遍就可以提供给自己和B服务Client使用。如果基于Rpc方式,A、B都需要查询一遍。微服务过多也不好啊!


通过改变服务提供方式,尽量减少Rpc调用次数和开销,从而优化整个系统的性能。


例如社交关注关系服务。在这个服务中,需要查询用户之间的关注关系。为了提高服务性能,关注服务内部使用缓存来存储关注关系。为了降低高并发场景下的调用延迟和机器负载,关注服务提供了一个Java Client Jar查询关注关系,放弃了上游调用rpc接口的方式。这样做的好处是可以减少一次Rpc调用,避免了下游服务因GC 停顿而导致的耗时。


2.6 单条调用改为批量调用


无论是查询还是写入,都可以使用批量调用来代替单条调用。比如,在查询用户订单的详情时,应该批量查询多个订单,而不是通过循环逐个查询订单详情。批量调用虽然会比单条调用稍微耗时多一些,但是循环调用的耗时却是单条调用的N倍,所以批量查询耗时要低很多。


在接口设计和代码流程中,我们应该尽量避免使用for循环进行单条查询或单条写入操作。正如此文所提到的,批量插入数据库的性能可能是单条插入的3-5倍。# 10亿数据如何插入Mysql,10连问,你想到了几个?


2.7 并行调用


在调用多个接口时,可以选择串行调用或并行调用的两种方式。串行调用是指依次调用每个接口,一个接口完成后才能调用下一个接口,而并行调用是指同时调用多个接口。可以看出并行调用的耗时更低,因为串行调用的耗时是多个接口耗时的总和,而并行调用的耗时是耗时最高的接口耗时。


为了灵活实现多个接口的调用顺序和依赖关系,可以使用Java中的CompletableFuture类。CompletableFuture可以将多个接口的调用任务编排成一个有序的执行流程,可以实现最大程度的并发查询或并发修改。


例如,可以并行调用两个接口,然后等待两个接口全部成功后,再对查询结果进行汇总处理。这样可以提高查询或修改的效率。


CompletableFuture<Void> first = CompletableFuture.runAsync(()->{  
            System.out.println("do something first");
Thread.sleep(200);
        });
        CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
            System.out.println("do something second");
Thread.sleep(300);
        });
        CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(first, second).whenComplete((m,k)->{
            System.out.println("all finish do something");
        });

allOfFuture.get();//汇总处理结果

CompletaleFuture 还支持自定义线程池,支持同步调用、异步调用,支持anyOf任一成功则返回等多种编排策略。由于不是本文重点,不再一一说明


2.8 提前过滤,减少无效调用


在某些活动匹配的业务场景里,相当多的请求实际上是不满足条件的,如果能尽早的过滤掉这些请求,就能避免很多无效查询。例如用户匹配某个活动时,会有非常多的过滤条件,如果该活动的特点是仅少量用户可参加,那么可首先使用人群先过滤掉大部分不符合条件的用户。


2.9 拆分接口


前面提到如果Http接口功能过于庞大,核心数据和非核心数据杂糅在一起,耗时高和耗时低的数据耦合在一起。为了优化请求的耗时,可以通过拆分接口,将核心数据和非核心数据分别处理,从而提高接口的性能。


而在Rpc接口方面,也可以使用类似的思路进行优化。当上游需要调用多个Rpc接口时,可以并行地调用这些接口。优先返回核心数据,如果处理非核心数据或者耗时高的数据超时,则直接降级,只返回核心数据。这种方式可以提高接口的响应速度和效率,减少不必要的等待时间。


3. 选择合适的存储系统


无论是查询接口还是写入接口都需要访问数据源,访问存储系统。读高写低,读低写高,读写双高等不同场景需要选择不同的存储系统。


3.1 MySQL 换 Redis


当系统查询压力增加时,可以把MySQL数据异构到Redis缓存中。


3.1.1 选择合适的缓存结构


Redis包含了一些常见的数据结构,包括字符串(String)、列表(List)、有序集合(SortSet)、哈希(Hash)和基数估计(HyperLogLog)、GEOHash等。


在不同的应用场景下,我们可以根据需求选择合适的数据结构来存储数据。举例来说,如果我们需要存储用户的关注列表,可以选择使用哈希结构(Hash)。对于需要对商品或文章的浏览量进行去重的情况,可以考虑使用基数估计结构(HyperLogLog)。而对于用户的浏览记录,可以选择列表(List)等结构来存储。如果想实现附近的人功能,可以使用Redis GEOHash结构。


Redis提供了丰富的API来操作这些数据结构,我们可以根据实际需要选择适合的数据结构和相关API来简化代码实现,提高开发效率。


关于缓存结构选择可以参考这篇文章。# 10W+TPS高并发场景【我的浏览记录】系统设计


3.1.2 选择合适的缓存策略


缓存策略指的是何时更新缓存和何时将缓存标记为过期或清理缓存。主要有两种策略。


策略1:是当数据更新时,更新缓存,并且在缓存Miss(即缓存中没有所需数据)时,从数据源加载数据到缓存中。


策略2:是将缓存设置为常驻缓存,即缓存永远不过期。当数据更新时,会即时更新缓存中的数据。这种策略通常会占用大量内存空间,因此一般只适用于数据量较小的情况下使用。另外,定时任务会定期将数据库中的数据更新到缓存中,以兜底缓存数据的一致性。


总的来说,选择何种缓存策略取决于具体的应用需求和数据规模。如果数据量较大,一般会选择策略1;而如果数据量较小且要求缓存数据的实时性,可以考虑策略2。


关于缓存使用,可以参考我的踩坑记录:#点击这里了解 第一次使用缓存翻车了


3.2 Redis 换 本地缓存


Redis相比传统数据库更快且具有更强的抗并发能力。然而,与本地缓存相比,Redis缓存仍然较慢。前面提到的Redis访问速度大约在3-5毫秒之间,而使用本地缓存几乎可以忽略不计。


如果频繁访问Redis获取大量数据,将会导致大量的序列化和反序列化操作,这会显著增加young gc频率,也会增加CPU负载。


本地缓存的性能更强,当使用Redis仍然存在性能瓶颈时,可以考虑使用本地缓存。可以设置多级缓存机制,首先访问本地缓存,如果本地缓存中没有数据,则访问Redis分布式缓存,如果仍然不存在,则访问数据库。通过使用多级缓存策略来实现更高效的性能。


本地缓存可以使用Guava Cahce 。参考本地缓存框架Guava Cache


也可以使用性能更强的Caffeine。点击这里了解


Redis由于单线程架构,在热点缓存应对上稍显不足。使用本地缓存可以极大的解决缓存热点问题。例如以下代码创建了Caffeine缓存,最大长度1W,写入后30分钟过期,同时指定自动回源取值策略。


public LoadingCache<String, User> createUserCache() {
return Caffeine.newBuilder()
.initialCapacity(1000)
.maximumSize(10000L)
.expireAfterWrite(30L, TimeUnit.MINUTES)
//.concurrencyLevel(8)
.recordStats()
.build(key -> userDao.getUser(key));
}

3.3 Redis 换 Memcached


当存在热点key和大key时,Redis集群的负载会变得不均衡,从而降低整个集群的性能。这是因为Redis是单线程执行的系统,当处理热点key和大key时,会对整个集群的性能产生影响。


相比之下,Memcached缓存是多线程执行的,它可以更好地处理热点key和大key的问题,因此可以更好地应对上述性能问题。如果遇到这些问题,可以考虑使用Memcached进行替代。


另外,还可以通过使用本地缓存并结合Redis来处理热点key和热点大key的情况。这样可以减轻Redis集群的负担,并提升系统的性能。


3.4 MySQL 换 ElasticSearch


在后台管理页面中,通常需要对列表页进行多条件检索。MySQL 无法满足多条件检索的需求,原因有两点。第一点是,拼接条件检索的查询SQL非常复杂且需要进行定制化,难以进行维护和管理。第二点是,条件检索的查询场景非常灵活,很难设计合适的索引来提高查询性能,并且难以保证查询能够命中索引。


相比之下,ElasticSearch是一种天然适合于条件检索场景的解决方案。无论数据量的大小,对于列表页查询和检索等场景,推荐首选ElasticSearch。


可以将多个表的数据异构到ElasticSearch中建立宽表,并在数据更新时同步更新索引。在进行检索时,可以直接从ElasticSearch中获取数据,无需再查询数据库,提高了检索性能。


3.5 MySQL 换 HBase


MySQL并不适合大数据量存储,若不对数据进行归档,数据库会一直膨胀,从而降低查询和写入的性能。针对大数据量的读写需求,可以考虑以下方法来存储订单数据。


首先,将最近1年的订单数据存储在MySQL数据库中。这样可以保证较高的数据库查询性能,因为MySQL对于相对较小的数据集来说是非常高效的。


其次,将1年以上的历史订单数据进行归档,并将这些数据异构(转储)到HBase中。HBase是一种分布式的NoSQL数据库,可以存储海量数据,并提供快速的读取能力。


在订单查询接口上,可以区分近期数据和历史数据,使得上游系统能够根据自身的需求调用适当的订单接口来查询订单详情。


在将历史订单数据存储到HBase时,可以设置合理的RowKey。RowKey是HBase中数据的唯一标识,在查询过程中可以通过RowKey来快速找到目标数据。通过合理地设置RowKey,可以进一步提高HBase的查询性能。


通过将订单数据分别存储在MySQL和HBase中,并根据需求进行区分查询,可以满足大数据量场景的读写需求。MySQL用于存储近期数据,以保证查询性能;而HBase用于存储归档的历史数据,并通过合理设置的RowKey来提高查询性能。


4.代码层次优化


4.1 同步转异步


将写请求从同步转为异步可以显著提升接口的性能。


以发送短信接口为例,该接口需要调用运营商接口并在公网上进行调用,因此耗时较高。如果业务方选择完全同步发送短信,就需要处理失败、超时、重试等与稳定性有关的问题,且耗时也会非常高。因此,我们需要采用同步加异步的处理方式。


公司的短信平台应该采用Rpc接口发送短信。在收到请求后,首先进行校验,包括校验业务方短信模板的合法性以及短信参数是否合法。待校验完成后,我们可以将短信发送任务存入数据库,并通过消息队列进行异步处理。而对业务方提供的Rpc接口的语义也发生了变化:我们成功接收了发送短信的请求,稍后将以异步的方式进行发送。至于发送短信失败、重试、超时等与稳定性和可靠性有关的问题,将由短信平台保证。而业务方只需确保成功调用短信平台的Rpc接口即可


4.2 减少日志打印


在高并发的查询场景下,打印日志可能导致接口性能下降的问题。我曾经不认为这会是一个问题,直到我的同事犯了这个错误。有同事在排查问题时顺手打印了日志并且带上线。第二天高峰期,发现接口的 tp99 耗时大幅增加,同时 CPU 负载和垃圾回收频率也明显增加,磁盘负载也增加很多。日志删除后,系统回归正常。


特别是在日志中包含了大数组或大对象时,更要谨慎,避免打印这些日志。


4.3 使用白名单打印日志


不打日志,无法有效排查问题。怎么办呢?


为了有效地排查问题,建议引入白名单机制。具体做法是,在打印日志之前,先判断用户是否在白名单中,如果不在,则不打印日志;如果在,则打印日志。通过将公司内的产品、开发和测试人员等相关同事加入到白名单中,有利于及时发现线上问题。当用户提出投诉时,也可以将相关用户添加到白名单,并要求他们重新操作以复现问题。


这种方法既满足了问题排查的需求,又避免了给线上环境增加压力。(在测试环境中,可以完全开放日志打印功能)


4.4 避免一次性查询过多数据


在进行查询操作时,应尽量将单次调用改为批量查询或分页查询。不论是批量查询还是分页查询,都应注意避免一次性查询过多数据,比如每次加载10000条记录。因为过大的网络报文会降低查询性能,并且Java虚拟机(JVM)倾向于在老年代申请大对象。当访问量过高时,频繁申请大对象会增加Full GC(垃圾回收)的频率,从而降低服务的性能。


建议最好支持动态配置批量查询的数量。当接口的性能较差时,可以通过动态配置批量查询的数量来优化接口的性能,根据实际情况灵活地调整每次查询的数量。


4.5 避免深度分页


深度分页指的是对一个大数据集进行分页查询时,每次只查询一页的数据,但是要获取到指定页数的数据,就需要依次查询前面的页数,这样查询的范围就会越来越大,导致查询效率变低。


在进行深度分页时,MySQL和ElasticSearch会先加载大量的数据,然后根据分页要求返回少量的数据。这种处理方式导致深度分页的效率非常低,同时也给MySQL和ElasticSearch带来较高的内存压力和CPU负载。因此,我们应该尽可能地避免使用深度分页的方式。


为了避免深度分页,可以采用每次查询时指定最小id或最大id的方法。具体来说,当进行分页查询时,可以记录上一次查询结果中的最小id或最大id(根据排序方式来决定)。在进行下一次查询时,指定查询结果中的最小id或最大id作为起始条件,从而缩短查询范围。这样每次只获取前N条数据,可以提高查询效率。


关于分页可以参考 我的文章# 四选一,如何选择适合你的分页方案?


4.6 只访问需要用到的数据


为了查询数据库和下游接口所需的字段,我们可以采取一些方法。例如,商品数据的字段非常多,如果每次调用都返回全部字段,将导致数据量过大。因此,上游可以指定使用的字段,从而有效降低接口的数据量,提升接口的性能。


这种方式不仅可以减少网络IO的耗时,而且还可以减少Rpc序列化和反序列化的耗时,因为接口的数据量较少。


对于访问量极大的接口来说,处理这些多余的字段将会增加CPU的负载,并增加Young GC的次数。因此不要把所有的字段都返回给上游!应该按需定制。


4.7 预热低流量接口


对于访问量较低的接口来说,通常首次接口的响应时间较长。原因是JVM需要加载类、Spring Aop首次动态代理,以及新建连接等。这使得首次接口请求时间明显比后续请求耗时长。


然而在流量较低的接口中,这种影响会更大。用户可能尝试多次请求,但依然经常出现超时,严重影响了用户体验。每次服务发布完成后,接口超时失败率都会大量上升!


那么如何解决接口预热的问题呢?可以考虑在服务启动时,自行调用一次接口。如果是写接口,还可以尝试更新特定的一条数据。


另外,可以在服务启动时手动加载对应的类,以减少首次调用的耗时。不同的接口预热方式有所不同,建议使用阿里开源的诊断工具arthas,通过监控首次请求时方法调用堆栈的耗时来进行接口的预热。


arthas使用文档 arthas.aliyun.com/doc/trace.h…


使用arthas trace命令可以查看 某个方法执行的耗时情况。
trace com.xxxx.ClassA function1
image.png


5. 数据库优化


5.1 读写分离


增加MySQL数据库的从节点来实现负载均衡,减轻主节点的查询压力,让主节点专注于处理写请求,保证读写操作的高性能。


除此之外,当需要跨地域进行数据库的查询时,由于较高网络延迟等问题,接口性能可能变得很差。在数据实时性不太敏感的情况下,可以通过在多个地域增加从节点来提高这些地域的接口性能。举个例子,如果数据库主节点在北京,可以在广州、上海等地区设置从节点,在数据实时性要求较低的查询场景,可有效提高南方地区的接口性能。


5.2 索引优化


5.2.1查询更新务必命中索引


查询和更新SQL必须命中索引。查询SQL如果没命中索引,在访问量较大时,会出现大量慢查询,严重时会导致整个MySQL集群雪崩,影响到其他表、其他数据库。所以一定要严格审查SQL是否命中索引。可以使用explain命令查看索引使用情况。


在SQL更新场景,MySQL会在索引上加锁,如果没有命中索引会对全表加锁,全表的更新操作都会被阻塞住。所以更新SQL更要确保命中索引。


因此,为了避免这种情况的发生,需要严格审查SQL是否命中索引。可以使用"explain"命令来查看SQL的执行计划,从而判断是否有使用索引。这样可以及早发现潜在的问题,并及时采取措施进行优化和调整。


除此之外,最好索引字段能够完全覆盖查询需要的字段。MySQL索引分主键索引和普通索引。普通索引命中后,往往需要再查询主键索引获取记录的全部字段。如果索引字段完全包含查询的字段,即索引覆盖查询,就无需再回查主键索引,可以有效提高查询性能。


更详细请参考本篇文章 # 深入理解mysql 索引特性


5.2.2 常见索引失效的场景



  1. 查询表达式索引项上有函数.例如date(created_at) = 'XXXX'等.字符处理等。mysql将无法使用相应索引

  2. 一次查询(简单查询,子查询不算)只能使用一个索引

  3. != 不等于无法使用索引

  4. 未遵循最左前缀匹配导致索引失效

  5. 类型转换导致索引失效,例如字符串类型指定为数字类型等。

  6. like模糊匹配以通配符开头导致索引失效

  7. 索引字段使用is not null导致失效

  8. 查询条件存在 OR,且无法命中索引。


5.2.3 提高索引利用率


当索引数量过多时,索引的数据量就会增加,这可能导致数据库无法将所有的索引数据加载到内存中,从而使得查询索引时需要从磁盘读取数据,进而大大降低索引查询的性能。举例来说,我们组有张表700万条数据,共4个索引,索引数据量就达到2.8GB。在一个数据库中通常有多张表,在进行分库分表时,可能会存在100张表。100张表就会产生280GB的索引数据,这么庞大的数据量无法全部放入内存,查询索引时会大大降低缓存命中率,进而降低查询和写入操作的性能。简而言之,避免创建过多的索引。


可以选择最通用的查询字段作为联合索引最左前缀,让索引覆盖更多的查询场景。


5.3 事务和锁优化


为了提高接口并发量,需要避免大事务。当需要更新多条数据时,避免一次性更新过多的数据。因为update,delete语句会对索引加锁,如果更新的记录数过多,会锁住太多的数据,由于执行时间较长,会严重限制数据库的并发量。


间隙锁是MySQL在执行更新时为了保证数据一致性而添加的锁定机制。虽然更新的记录数量很少,但MySQL可能会锁定比更新数量更大的范围。因此,需要注意查询语句中的where条件是否包含了较大的范围,这样可能会锁定不应该被锁定的记录。


如果有批量更新的情况,需要降低批量更新的数量,缩小更新的范围。


其次在事务内可能有多条SQL,例如扣减库存和新增库存扣减流水有两条SQL。因为两个SQl在同一个事务内,所以可以保证原子性。但是需要考虑两个SQL谁先执行,谁后执行?


建议先扣库存,再增加流水。


扣减库存的更新操作耗时较长且使用了行锁,而新增流水的速度较快但是串行执行,如果先新增流水再扣减库存,会导致流水表被锁定的时间更长,限制了流水表的插入速度,同时会阻塞其他扣减库存的事务。相反,如果先扣减库存再新增流水,流水表被锁定的时间较短,有利于提高库存扣减的并发度。


5.4 分库分表,降低单表规模


MySQL单库单表的性能瓶颈很容易达到。当数据量增加到一定程度时,查询和写入操作可能会变得缓慢。这是因为MySQL的B+树索引结构在单表行数超过2000万时会达到4层,同时索引的数据规模也会变得非常庞大。如果无法将所有索引数据都放入内存缓存中,那么查询索引时就需要进行磁盘查询。这会导致查询性能下降。参考# 10亿数据如何插入Mysql,10连问,你想到了几个?


为了克服这个问题,系统设计在最初阶段就应该预测数据量,并设置适合的分库分表策略。通过将数据分散存储在多个库和表中,可以有效提高数据库的读写性能。此外,分库分表也可以突破单表的容量限制。


分库分表工具推荐使用 Sharding-JDBC


5.5 冗余数据,提高查询性能


使用分库分表后,索引的使用受到限制。例如,在关注服务中,需要满足两个查询需求:1. 查询用户的关注列表;2. 查询用户的粉丝列表。关注关系表包含两个字段,即关注者的fromUserId和被关注者的toUserId。


对于查询1,我们可以指定fromUserId = A,即可查询用户A的关注列表。


对于查询2,我们可以指定toUserId = B,即可查询用户B的粉丝列表。


在单库单表的情况下,我们可以设计fromUserId和toUserId这两个字段作为索引。然而,当进行分库分表后,我们面临选择哪个字段作为分表键的困扰。无论我们选择使用fromUserId还是toUserId作为分表键,都会导致另一个查询场景变得难以实现。


解决这个问题的思路是:存储结构不仅要方便写入,还要方便查询。既然查询不方便,我们可以冗余一份数据,以便于查询。我们可以设计两张表,即关注列表表(Follows)和粉丝列表表(Fans)。其中,Follows表使用fromUserId作为分表键,用于查询用户的关注列表;Fans表使用toUserId作为分表键,用于查询用户的粉丝列表。


通过冗余更多的数据,我们可以提高查询性能,这是常见的优化方案。除了引入新的表外,还可以在表中冗余其他表的字段,以减少关联查询的次数。


关注关系设计 请参考 #解密亿级流量【社交关注关系】系统设计


5.6 归档历史数据,降低单表规模


MySQL并不适合存储大数据量,如果不对数据进行归档,数据库会持续膨胀,从而降低查询和写入的性能。为了满足大数据量的读写需求,需要定期对数据库进行归档。


在进行数据库设计时,需要事先考虑到对数据归档的需求,为了提高归档效率,可以使用ctime(创建时间)进行归档,例如归档一年前的数据。


可以通过以下SQL语句不断执行来归档过期数据:


delete from order where ctime < ${minCtime} order by ctime limit 100;


需要注意的是,执行delete操作时,ctime字段应该有索引,否则将会锁住整个表


另外,在将数据库数据归档之前,如果有必要,一定要将数据同步到Hive中,这样以后如果需要进行统计查询,可以使用Hive中的数据。如果归档的数据还需要在线查询,可以将过期数据同步到HBase中,这样数据库可以提供近期数据的查询,而HBase可以提供历史数据的查询。可参考上述MySQL转HBase的内容。


5.7 使用更强的物理机 CPU/内存/SSD硬盘


MySQL的性能取决于内存大小、CPU核数和SSD硬盘读写性能。为了适配更强的宿主机,可以进行以下MySQL优化配置


innodb_buffer_pool_size


缓冲池是数据和索引缓存的地方。默认大小为128M。这个值越大越好决于CPU的架构,这能保证你在大多数的读取操作时使用的是内存而不是硬盘。典型的值是5-6GB(8GB内存),20-25GB(32GB内存),100-120GB(128GB内存)。


max_connections


数据库最大连接数。可以适当调大数据库链接


innodb_flush_log_at_trx_commit


控制MySQL刷新数据到磁盘的策略。



  1. 默认=1,即每次事务提交都会刷新数据到磁盘,安全性最高不会丢失数据。

  2. 当配置为0、2 会每隔1s刷新数据到磁盘, 在系统宕机、mysql crash时可能丢失1s的数据。


innodb_thread_concurrency


innodb_thread_concurrency默认是0,则表示没有并发线程数限制,所有请求都会直接请求线程执行。



当并发用户线程数量小于64,建议设置innodb_thread_concurrency=0;
在大多数情况下,最佳的值是小于并接近虚拟CPU的个数;



innodb_read_io_threads


设置InnoDB存储引擎的读取线程数。默认值是4,表示使用4个线程来读取数据。可以根据服务器的CPU核心数来调整这个值。例如调整到16甚至32。


innodb_io_capacity


innodb_io_capacity InnoDB可用的总I/O容量。该参数应该设置为系统每秒可以执行的I/O操作数。该值取决于系统配置。当设置innodb_io_capacity时,主线程会根据设置的值来估算后台任务可用的I/O带宽


innodb_io_capacity_max: 如果刷新操作过于落后,InnoDB可以超过innodb_io_capacity的限制进行刷新,但是不能超过本参数的值


默认情况下,MySQL 分别配置了200 和2000的默认值。
image.png


当磁盘为SSD时,可以考虑设置innodb_io_capacity= 2000,innodb_io_capacity_max=4000


6. 压缩数据


6.1 压缩数据库和缓存数据


压缩文本数据可以有效地减少该数据所需的存储空间,从而提高数据库和缓存的空间利用率。然而,压缩和解压缩的过程会增加CPU的负载,因此需要仔细考虑是否有必要进行数据压缩。此外,还需要评估压缩后数据的效果,即压缩对数据的影响如何。


例如下面这一段文字我们使用GZIP 进行压缩



假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。



该段文字使用UTF-8编码,共570位byte。使用GZIP 压缩后,变为328位Byte。压缩效果还是很明显的。


压缩代码如下


//压缩
public static byte[] compress(String str, String encoding) {
if (str == null || str.length() == 0) {
return null;
}
byte[] values = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes(encoding));
gzip.close();
values = out.toByteArray();
out.close();
} catch (IOException e) {
log.error("gzip compress error.", e);
throw new RuntimeException("压缩失败", e);
}
return values;
}
// 解压缩
public static String uncompressToString(byte[] bytes, String encoding) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
String value = out.toString(encoding);
out.close();
return value;
} catch (IOException e) {
log.error("gzip uncompress to string error.", e);
throw new RuntimeException("解压缩失败", e);
}
}

值得一提的是使用GZIP压缩算法的cpu负载和耗时都是比较高的。使用压缩非但不能起到降低接口耗时的效果,可能导致接口耗时增加,要谨慎使用。除此之外,还有其他压缩算法在压缩时间和压缩率上有所权衡。可以选择适合的自己的压缩算法。


image.png


7. 系统优化


7.1 优化GC


无论是Young GC还是Full GC,在进行垃圾回收时都会暂停所有的业务线程。因此,需要关注垃圾回收的频率,以确保对业务的影响尽可能小。


插播提问:为什么young gc也需要stop the world ? 阿里面试官问我的,把我问懵逼了。


一般情况下,通过调整堆大小和新生代大小可以解决大部分垃圾回收问题。其中,新生代是用于存放新创建的对象的区域。对于Young GC的频率增加的情况,一般是系统的请求量大量增长导致。但如果young gc增长非常多,就需要考虑是否需要增加新生代的大小。


因为如果新生代过小,很容易被打满。这导致本可以被Young GC掉的对象被晋升(Promotion)到老年代,过早地进入老年代。这样一来,不仅Young GC频繁触发,Full GC也会频繁触发。


gc场景非常多,建议参考美团的技术文章详细概括了9种CMS GC问题。# Java中9种常见的CMS GC问题分析与解决


7.2 提升服务器硬件


如果cpu负载较高 可以考虑提高每个实例cpu数量,提高实例个数。同时关注网络IO负载,如果机器流量较大,网卡带宽可能成为瓶颈。


高峰期和低峰期如果机器负载相差较大,可以考虑设置弹性伸缩策略,高峰期之前自动扩容,低峰期自动缩容,最大程度提高资源利用率。


8. 交互优化


8.1 调整交互顺序


我曾经负责过B端商品数据创建,当时产品提到创建完虚拟商品后要立即跳转到商品列表页。当时我们使用ElasticSearch 实现后台管理页面的商品查询,但是ElasticSearch 在新增记录时,默认是每 1 秒钟构建1次索引,所以如果创建完商品立即跳转到商品列表页是无法查到刚创建的商品的。于是和产品沟通商品创建完成跳转到商品详情页是否可以,沟通后产品也认可这个交互。


于是我无需调整ElasticSearch 构建索引的时机。(后来了解到 ElasticSearch 提供了API。新增记录后,可立即构建索引,就不存在1秒的延迟了。但是这样操作索引文件会非常多,影响索引查询性能,不过后台管理对性能要求不高,也能接收。)


通过和产品沟通交互和业务逻辑,有时候能解决很棘手的技术问题。有困难,不要闷头自己扛哦~


8.2 限制用户行为


在社交类产品中用户关注功能。如果不限制用户可以关注的人数,可能会出现恶意用户大量关注其他用户的情况,导致系统设计变得复杂。


为了判断用户A是否关注用户B,可以查看A的关注列表中是否包含B,而不是检查B的粉丝列表中是否包含A。这是因为粉丝列表的数量可能非常庞大,可能达到上千万。而正常用户的关注列表通常不会很多,一般只有几百到几千人。


为了提高关注关系的查询性能,可将关注列表数据导入到Redis Hash结构中。系统通过限制用户的最大关注上限,避免出现Redis大key的情况,也避免大key过期时的性能问题,保证集群的整体性能的稳定。避免恶意用户攻击系统。


可以看这篇文章 详细了解关注系统设计。# 解密亿级流量【社交关注关系】系统设计


作者:他是程序员
来源:juejin.cn/post/7287420810318299190
收起阅读 »

ThreadLocal使用不规范,上线两行泪

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜 ThreadLocal是Java中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点...
继续阅读 »

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜





ThreadLocalJava中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点,对于此网上已经有很多经典文章来进行分析,但今天我们主要分析笔者在项目中遇到的一个错误使用ThreadLocal的示例,并针对错误原因进行深入剖析,理论结合实践让你更加透彻的理解ThreadLocal的使用。


前言


Java中的ThreadLocal是一种用于在多线程环境中存储线程局部变量的机制,它为每个线程都提供了独立的变量副本,从而避免了线程之间的竞争条件。事实上,ThreadLocal的工作原理是在每个线程中创建一个独立的变量副本,并且每个线程只能访问自己的副本。


进一步,ThreaLocal可以在当前线程中独立的保存信息,这样就方便同一个线程的其他方法获取到该信息。 因此,ThreaLocal的一个最广泛的使用场景就是将信息保存,从而方便后续方法直接从线程中获取。


使用ThreadLocal出现的问题


明白了ThreaLocal的应应用场景后,我们来看一段如下代码:



控制层



@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

@Autowire
private UserService userService;

@GetMapping("get-userdata-byId")
public CommonResult<Object> getUserData(Integer uid) {

return userService.getUserInfoById(uid);

}


服务层



@Service
public class UserService {

ThreadLocal<UserInfo> locals = new ThreadLocal<>();

public CommonResult<UserInfo> getUserInfoById ( String uid) {
UserInfo info = locals.get();

if (info == null) {
// 调用uid查询用户
UserInfo userInfo = UserMapper.queryUserInfoById(uid);
locals.set(userInfo);
}
// ....省略后续会利用UserInfo完成某些操作

return CommonResult.success(info);
}
}

(注:此处为了方便复现项目代码进行了简化,重点在于理解ThreaLocal的使用)


先来简单介绍一下业务逻辑,前台通过url访问/user/get-userdata-byId后,后端会根据传入的uid信息查询用户信息,以避免进而根据用户信息执行相应的处理逻辑。进一步,在服务层中会缓存当前id对应的用户信息,避免频繁的查询数据库。


直观来看,上述代码似乎没问题。但最近用户反馈会出现这样一个问题,就是用户A登录系统后,查询到的可能是用户B的信息,这个问题就很诡异。遇到问题不要慌,不妨来看看笔者是如何进行思考,来定位,解决问题的。


首先,用户A登录系统后,前端访问/user/get-userdata-byId时携带的uid信息肯定是用户Auid信息;进一步,传到控制层getUserData处的uid信息肯定是用户Auid。所以,发生问题一定发生在UserService中的getUserInfoById方法。


进一步,由于用户传入的uid信息没有问题,那么传入getUserInfoById方法也肯定没有问题,所以问题发生地一定在getUserInfoById中获取用户信息的位置。所以不难得出这样的猜测,即问题大概率在 UserInfo info = locals.get()这行代码。


为了加深理解,我们再来回顾一下问题。"即用户A登录,最终却查询到用户B相关的信息"。 其实,这个问题本质其实在于数据不一致。众所周知,造成数据不一致的原因有很多,但归根到底其实无非就是:“存在多线程访问的资源信息,进一步,多线程的存在导致数据状态的改变原因不唯一”


Spring中的Bean都是单例的,也就是说Bean中成员信息是共享的。换句话说, 如果Bean中会操纵类的成员变量,那么每次服务请求时,都会对该变量状态进行改变,也就会导致该变量成员那状态不断发生改变。


具体到上述例子,UserService中的被方法操纵的成员是什么?当然是locals这个成员变量啦! 至此,问题其实已经被我们定位到了,导致问题发生的原因在于locals变量。


说到此,你可能你会疑惑ThreadLocal不是可以保证线程安全吗?怎么使用了线程安全的工具包还会导致线程安全问题?


问题复现


况且你说是ThreadLocal出问题那就是ThreadLocal出问题吗?你有证据吗?所以,接下来我们将通过几行简单的代码,复现这个问题。



@RestController
@RequestMapping("/th")
public class UserController {

ThreadLocal<Integer> uids = new ThreadLocal<>();

@GetMapping("/u")
public CommonResult getUserInfo(Integer uid) {
Integer firstId = uids.get();
String firstMsg = Thread.currentThread().getName() + " id is " + firstId;
if (firstId == null) {
uids.set(uid);
}

Integer secondId = uids.get();
String secondMsg = Thread.currentThread().getName() + " id is " + secondId;

List<String> msgs = Arrays.asList(firstMsg,secondMsg);
return CommonResult.success(msgs);


}
}


  1. 第一次访问:uid=1


image.png



  1. 第二次访问:uid=2
    image.png


可以看到,对于第二次uid=2的访问,这次就出现了 Bug,显然第二次获取到了用户1的信息。其实,从这里就可以看出,我们最开始的猜测没有任何问题。


拆解问题发生原因


既然知道了发生问题的原因在于ThreadLocal的使用,那究竟是什么导致了这个问题呢?事实上,我们在使用ThreadLocal时主要就是使用了其的get/set方法,这就是我们分析的切入口。先来看下ThreadLocalset方法。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

可以看到,ThreadLocalset方法逻辑大致如下:



  1. 首先,通过Thread.currentThread获取到当前的线程

  2. 然后,获取到线程当中的属性ThreadLocalMap。接着,对ThreadLocalMap进行判断,如果不为空,就直接更新要保存的变量值;否则,创建一个threadLocalMap,并且完成赋值。


进一步,下图展示了Thrad,ThreadLocal,ThredLocalMap三者间的关系。


image.png


回到我们例子,那导致出现访问错乱的原因是什么呢?其实很简单,原因就是 Tomcat 内部会维护一个线程池,从而使得线程被重用。从图中可以看到两次请求的线程都是同一个线程: http-nio-8080-exec-1,所以导致数据访问出现错乱。


image.png


那有什么解决办法吗?其实很简单,每次使用完记得执行remove方法即可。因为如果不调用remove方法,当面临线程池或其他线程重用机制可能会导致不同任务之间共享ThreadLocal数据,这可能导致意外的数据污染或不一致性。就如我们的例子那样。


总结


至此,我们以一个实际生产中遇到的一个问题为例由浅入深的分析了ThreadLocal使用不规范所带来的线程不安全问题。可以看到排查问题时,我们用到的不仅仅只有ThreadLocal的知识,更有多线程相关的知识。


可能平时我们也会抱怨学了很多线程知识,但工作中却很少使用。因为日常代码中基本写不到多线程相关的功能。但事实却是,很多时候只是我们没有意识到多线程的使用。例如,在Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境,否则接口也不可能支持这么高的并发,并不能单纯认为没有显式开启多线程就不会有线程安全问题。此外,虽然jdk提供很多线程安全的工具类,但其也有特定的使用规范,如果不遵循规范依旧会导致线程安全问题, 并不是使用了线程安全的工具类就一定不会出问题!


最后,再多提一嘴,学了的知识一定要用起来,可能你为了应付面试也曾看过ThreadLocal相关的面经,也知道使用ThreadLocal要执行remove,否则可能会导致内存泄露但编程的很多东西,确实需要自己实际操作,否则知识并不会凭空进入你的脑海。


选择了程序员这条路,注定只能不断的学习,大家一起共勉啦!另外,祝大家双节快乐!


作者:毅航
来源:juejin.cn/post/7283692887573184547
收起阅读 »

降低代码可读性的 12 个技巧

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。 假设一个叫”二狗“ 的程序员,喜欢做以下事情。 1. 二狗积极拆分微服务,一个表对应一个微服务 二狗十分认可微服务的设计...
继续阅读 »

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。


假设一个叫”二狗“ 的程序员,喜欢做以下事情。


1. 二狗积极拆分微服务,一个表对应一个微服务


二狗十分认可微服务的设计思想。认为微服务可以独立开发和发布,每次改动不会影响其他系统。大大提高了开发人员的效率和线上稳定性。还可以在新服务里使用新的技术,例如JDK 21


于是狗哥把微服务的思想发挥到极致,每一张表都是一个服务。系统的应用架构图十分壮观。狗哥自豪的跟新同学讲解自己设计的系统。新同学看着十几个服务陷入了思考,不停地问着每个服务的作用,干了什么。狗哥很满足。


新同学第一次开发需求,表现很差。虽然他要改10个服务,但是每个服务只改动了一点点。并且由于服务之间都是Rpc调用,需要定义大量的接口,他需要发布好多的 jar,定义版本号,解决测试环境版本冲突,测试和上线阶段可把他忙坏了。


光是梳理上线顺序,新同学就请教了狗哥 三次。 最后还是狗哥帮他上线了3 个服务,新同学才赶在 凌晨 3 点前把所有的服务发完。看着新同学买了奶茶的份上,狗哥这次才没有和领导吐槽,“这个同学不行啊,上个线都这么费劲”


微服务过多,也困扰着狗哥。虽然线上流量不高,但是由于 “微服务太多,系统架构复杂",接口性能不行。


于是狗哥开始进行重构,他重新加了一个开关,新逻辑可以减少Rpc,调用提高性能。狗哥在代码中加了注释 "新逻辑"。


狗哥把代码上线了,但是在线上环境不敢放开,只在测试环境打开了开关。


2. 二狗积极重构代码,但是线上不放量


狗哥喜欢对代码进行重构,狗哥和领导吹牛,说“ 重构后的代码性能更强,更稳定”。 狗哥还添加了注释 ”这是新逻辑“。


但是狗哥在线上比较谨慎,并没有进行放量。只是在测试环境,放开了全量。


新接手的同学不知道线上还没放量,看到“这是新逻辑” ,他就在狗哥的“新逻辑”上改代码。测试环境验证一切正常,到了线上阶段却怎么也跑不通。


此时新同学才发现 ”新逻辑“ 的开关没有打开,你猜,他敢打开这个开关吗? 于是他只能删代码,在旧逻辑上重新开发。 等到改完代码,再上线时,已经天亮了。


由于这次上线问题,大家一起熬夜加班,需求上线被推迟。新同学被产品和测试一顿骑脸输出。新同学委屈的想要离职。


3. 二狗喜欢挑战自我,方法长度一定要超过1000行


二狗写代码天马行空。二狗认为提炼新方法会打断自己的编码思路,代码越长,逻辑越连贯,可读性越高。二狗还认为 优秀的程序员写的方法都是 非常长的。这能体现个人的能力。


二狗不光自己写超长的方法,在改别人的代码时,也从不提炼新的方法。二狗总是在原来的方法中添加更长的一段代码。


新同学接手代码时速度很慢,即使加班到凌晨,也不理解狗哥代码设计的艺术。狗哥还向领导抱怨,”你最近招的人不行啊,一个小需求开发这么久,上线还出了bug。“


4. 二狗喜欢挑战自我,一个方法 if/try/else 要嵌套10层以上


二狗写代码十分认真,想到哪里就写哪里。 if/else/try catch 层层嵌套。 狗哥的思路很快,并且思考全面,
嵌套十几层的代码一点bug都没有,测试同学都夸赞狗哥 ”代码质量真高啊“,一个bug都没有。


新同学接手新代码时,看到嵌套十几层的代码,大脑瞬间就要爆炸。想要骂人,但是看到代码作者是狗哥……


无奈之下,自己实在看不懂这段代码,于是点了一杯奶茶,走到了狗哥工位旁,”狗哥,多喝点水,给你点了一杯奶茶。…………这段代码能给我讲讲吗?“


狗哥过几天和领导闲聊天,“新来的同学人不错,还给我点奶茶喝”


5. 二狗认为变量命名是艺术,要随机命名,不要和业务逻辑有关系


二狗觉得写代码是艺术,就好像画画一样。”你见过几个人能看懂 梵高的画?” 狗哥曾经和旁边人吹牛。


二狗写代码思路十分奇特,有时候来不及想变量如何命名,有时候是懒得想变量命名。狗哥经常随便就命名了,例如 str1,str2,list1,list2等等。不得不说,狗哥的思维还是敏捷的,这么多变量命名都能记住,还不出bug。


但是狗哥记性不大行,过一两个月就不太记得这些变量的意义了。


6. 二狗积极写注释,但是写了错误的注释


一个成熟稳重的程序员改别人代码时会十分慎重,如果有代码注释,他们一定会十分认真阅读并尝试理解它。


二狗喜欢把注释引入错误的方向,例如 “是” 改成 “不是”,“更好”改成”更差“,把两处不相干的注释交换一下位置 等。


新接手的同学点了一杯奶茶,虚心求助二狗,“狗哥,你写的这段注释有什么深意啊,我看了三天,也不理解啊”。


到时候狗哥就可以给新同学一边装B,一边讲代码了。当然还要看心情,要是不口渴,可以讲讲。


7. 二狗改代码很认真,但是注释从来不改


二狗改代码真的非常认真,但是他不喜欢改注释。最终代码大改特改,注释纹丝不动。最终代码和注释不相干,部分正确,部分错误。


新接手的同学研究了两天也没搞明白。于是求助了狗哥


到时候狗哥就可以大展神威了 。”那段注释是错的,你别管,就当没有!“


狗哥顺便还说了一句,”优秀的代码不需要写注释,也不知道是哪个XX 写的注释“,成功收割新同学的"钦佩"之情。


8. 二狗喜欢复制代码


狗哥写代码十分着急,根本来不及重构。他总是想到一段代码,就复制过来。神奇的是,狗哥经常这么写,但是也没出什么问题。


但新同学就惨了,在改完狗哥的代码后,总被测试同学背地里吐槽,“一点小需求咋这么多bug,跟狗哥比差远了”。原来新同学改了一处,忘了改另外几处,代码被复制了好多遍,他实在无法全面梳理。


于是每次代码写完,新同学都要不停的研究代码,总是害怕自己少改了哪些地方,下班时间越来越晚。并且新同学也不敢把雷同的代码重构到一起。(“你们猜猜他为什么不敢?)


慢慢的,组里的人都被迫向狗哥学习,狗哥成功输出了自己的编码习惯。


9. 二狗积极写技术方案,但是最终代码实现不按照技术方案来


二狗非常喜欢写技术方案,大部分时间都花在技术方案上,总是把技术方案打磨的 滑不留手。 但是在写代码时,狗哥总觉得按照方案设计写代码,时间上根本来不及啊,还是简单来吧,凑活实现吧。


例如狗哥曾经设计了一套复杂的Redis秒杀库存系统,但是实现时选择了最Low的 数据库同步扣减方案。


狗哥写的流程图和实际代码也没什么关系。 但是流程图旁边加满了注释和说明,让人觉得 ”这个技术方案很权威“。


新同学熟悉项目时,从公司文档中搜到了很多技术方案,本以为可以很快熟悉系统,但是发现技术方案和代码不太一样。越看越迷惑。


于是点了奶茶再次走向了狗哥,狗哥告诉他,“那个技术方案太复杂,排期紧张,开发来不及。你就当没那个技术方案。”


10. 二狗十分自信,从不打日志。


二狗对自己的代码十分自信,认为不会出现任何问题,所以他从来不打日志。每次开发代码时,狗哥的思维天马行空,但是从来不想加个日志会有助于排查问题。


直到有一天,线上真的出问题了,除了异常堆栈,找不到其他有效的日志。大家面面相觑,不知道怎么办。狗哥挺身而出,重新加了日志,上线。 故障持续了不知道有多久……,看着狗哥忙碌,领导不停地询问还需要多久才能上线。


复盘会上,有人对狗哥不写日志的行为进行批判,狗哥却在 狡辩 “加了日志,就能避免这次故障吗? 出问题还不是因为你们系统出了bug,跟我不打日志有啥关系。” 双方陷入了无限的扯皮之中……


11. 二狗积极学习,引用一个高大上的框架 解决一个小问题


二狗非常喜欢学习,学习了很多高大上的框架。最近二狗学习了规则引擎,觉得这是个好东西,恰好最近在进行重构。于是二狗把 drools、avatior、SPEL等规则引擎、表达式求值 等框架引入系统。只是为了解决策略模式的问题。即何种条件下使用哪种策略。 狗哥在系统架构图里,着重讲了规则引擎部分,十分自豪。


新同学熟悉系统后,光是规则引擎部分就看了足足一周。但是还是不知道怎么修改代码。于是向狗哥请教。狗哥告诉他说," 你在这个地方 加一行代码 rule.type == 12 ,走这个 CommonStrategy 策略类就可以了。“


新同学恍然大悟,原来这就是规则引擎啊。但是为什么不用策略模式呢?好像策略模式不费事啊! 狗哥技术就是强啊,杀鸡用核弹。


12. 二狗积极造轮子,能造轮子的程序员才是牛掰的程序员


二狗非常喜欢造轮子,他对开源软件的大神们心向往之,觉得自己应该向他们学习。狗哥认为 造轮子才能更快地成长。


于是在狗哥的积极学习下,组里的 分布式锁 没有使用 redission,而是自己用setnx搞的。虽然后面出了问题,但是狗哥的技术得到了锻炼。# 不用Redssion硬造轮子,结果翻车了…


总结


降低代码可读性的方式方法 包括但不限于以上12种;


像二狗这样的程序员包括但不限于二狗。


大家不要向二狗学习,因为他是真的。


作者:他是程序员
来源:juejin.cn/post/7286155742850449471
收起阅读 »

DNS

DNS DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都...
继续阅读 »

DNS


DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都只负责管理一个有限范围(一个或几个域)内的主机域 名和 IP 地址的对应关系,这些特定的 DNS 域或 IP 地址段称为 zone(区域)。根据地址解 析的方向不同,DNS 区域相应地分为正向区域(包含域名到 IP 地址的解析记录)和反向区 域(包含 IP 地址到域名的解析记录)

根域: 全球根服务器节点只有13个,10个在美国,1个荷兰,1个瑞典,1个日本


  • 一级域名:Top Level Domain: tld
  • 三类:组织域、国家域(.cn, .ca, .hk, .tw)、反向域
  • com, edu, mil, gov, net, org, int,arpa
  • 二级域名:magedu.com
  • 三级域名:study.magedu.com
  • 最多可达到127级域名

ICANN(The Internet Corporation for Assigned Names and Numbers)互联网名称与数字地址分配机构,负责在全球范围内对互联网通用顶级域名(gTLD)以及国家和地区顶级域名(ccTLD)系统的管理、以及根服务器系统的管理


DNS服务器类型


  • 缓存域名服务器:只提供域名解析结果的缓存功能,目的在于提高查询速度和效率, 但是没有自己控制的区域地址数据。构建缓存域名服务器时,必须设置根域或指定其他 DNS 服务器作为解析来源。
  • 主域名服务器:管理和维护所负责解析的域内解析库的服务器
  • 从域名服务器 从主服务器或从服务器"复制"(区域传输)解析库副本

序列号:解析库版本号,主服务器解析库变化时,其序列递增

刷新时间间隔:从服务器从主服务器请求同步解析的时间间隔

重试时间间隔:从服务器请求同步失败时,再次尝试时间间隔

过期时长:从服务器联系不到主服务器时,多久后停止服务

通知机制:主服务器解析库发生变化时,会主动通知从服务器


DNS查询类型及原理


查询方式

  • 递归查询:一般客户机和本地DNS服务器之间属于递归查询,即当客户机向DNS服务器发出请求后,若DNS服务器本身不能解析,则会向另外的DNS服务器发出查询请求,得到最终的肯定或否定的结果后转交给客户机。此查询的源和目标保持不变,为了查询结果只需要发起一次查询。(不需要自己动手)

  • 迭代查询:一般情况下(有例外)本地的DNS服务器向其它DNS服务器的查询属于迭代查询,如:若对方不能返回权威的结果,则它会向下一个DNS服务器(参考前一个DNS服务器返回的结果)再次发起进行查询,直到返回查询的结果为止。此查询的源不变,但查询的目标不断变化,为查询结果一般需要发起多次查询。(需要自己动手)


查询原理过程


正向解析查询过程:

1 先查本机的缓存记录

2 查询hosts文件

3 查询dns域名服务器,交给dns域名服务器处理 以上过程称为递归查询:我要一个答案你直接会给我结果

4 这个dns服务器可能是本地域名服务器,也有个缓存,如果有直接返回结果,如果没有则进行下一步

5 求助根域服务器,根域服务器返回可能会知道结果的一级域服务器,让他去找一级域服务器

6 求助一级域服务器,一级域服务器返回可能会知道结果的二级域服务器让他去找二级域服务器

7 求助二级域服务器,二级域服务器查询发现是我的主机,把查询到的ip地址返回给本地域名服务器

8 本地域名服务器将结果记录到缓存,然后把域名和ip的对应关系返回给客户端


DNS的分布式互联网解析库 



正向解析


各种资源记录


区域解析库:由众多资源记录RR(Resource Record)组成

记录类型:A, AAAA, PTR, SOA, NS, CNAME, MX


  • SOA:Start Of Authority,起始授权记录;一个区域解析库有且仅能有一个SOA记录,必须位于解析库的第一条记录SOA,是起始授权机构记录,说明了在众多 NS 记录里哪一台才是主要的服务器。在任何DNS记录文件中,都是以SOA ( Startof Authority )记录开始。SOA资源记录表明此DNS名称服务器是该DNS域中数据信息的最佳来源。
  • A(internet Address):作用,域名解析成IP地址
  • AAAA(FQDN): --> IPV6
  • PTR(PoinTeR):反向解析,ip地址解析成域名
  • NS(Name Server):,专用于标明当前区域的DNS服务器,服务器类型为域名服务器
  • CNAME : Canonical Name,别名记录
  • MX(Mail eXchanger)邮件交换器
  • TXT:对域名进行标识和说明的一种方式,一般做验证记录时会使用此项,如:SPF(反垃圾邮件)记录,https验证等

安装配置实操


下载安装bind文件,关闭防火墙进行后续操作

cd到etc下的文件夹查询对应软件,编辑named.conf文件




wq保存退出,接下来修改named.rfc1912.zones文件




wq保存退出后cd到var/named的文件下,复制local的文件作为自定义网站的文件模板进行编辑



wq保存退出之后检查有效性,首先修改网卡DNS为当前主机地址然后重启



 接下来开启程序systemctl start named




反向解析


和正向相似,在named.rfc1912文件下添加一段命令之后重新创建一个zones文件,将类型A换成PTR




主从复制


首先需要两台服务器,以我自己的192.168.222.100和192.168.222.200为例

进入etc下的named.conf文件修改两个any




修改etc下的named.rfc1912文件


 

 复制一份named.localhost作为模板进行修改
 

对网卡配置进行修改,然后重启网卡和启动named程序 


 接下来对第二台从服务器进行修改
同上,对named.conf文件进行修改两个any



 接下来修改从服务器的rfc文件




此时自动在slave文件夹下生成主服务器的文件



 修改主服务器的配置,双方重启后从也会变更









作者:钟情
链接:https://juejin.cn/post/7263410834499715131
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何制作 GitHub 个人主页

iOS
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。

./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:

./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:


  • README中定义一个放置动态内容的地方
  • scripts/中添加一个脚本,用来完成爬取工作
  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本

现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:

### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:

require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:

name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:


  • 使用 actions/checkout@v2操作来签出仓库。
  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。
  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogiri 和 octokit)。
  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。

有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


作者:chuck
链接:https://juejin.cn/post/7251884086537650232
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员要学会“投资知识”

iOS
啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢? 然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。 不幸的是,它们是有限的资产。随着新技术的...
继续阅读 »

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢?


然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。


不幸的是,它们是有限的资产。随着新技术的出现和语言环境的发展,你的知识可能会过时。不断变化的市场力量可能会使你的经验变得陈旧和无关紧要。考虑到技术和社会变革的加速步伐,这可能会发生得特别迅速。


随着你的知识价值的下降,你在公司或客户那里的价值也会降低。我们希望阻止所有这些情况的发生。


学习新知识的能力是你最关键的战略资产。但如何获取学习的方法,知道要学什么呢?


知识投资组合。


我们可以将程序员对计算过程、其工作应用领域的了解以及所有经验视为他们的知识投资组合。管理知识投资组合与管理金融投资组合非常相似:


1、定期的投资者有定期投资的习惯。


2、多样化是长期成功的关键。


3、聪明的投资者在投资组合中平衡保守和高风险高回报的投资。


4、投资者在低点买入,在高点卖出以获取最大回报。


5、需要定期审查和重新平衡投资组合。


为了在职业生涯中取得成功,你必须遵循相同的指导原则管理你的知识投资组合。


好消息是,管理这种类型的投资就像任何其他技能一样 - 它可以被学会。诀窍是从一开始就开始做,并养成习惯。制定一个你可以遵循并坚持的例行程序,直到它变成第二天性。一旦达到这一点,你会发现自己自动地吸收新的知识。


建立知识投资组合。


· 定期投资。 就像金融投资一样,你需要定期地投资你的知识投资组合,即使数量有限。习惯本身和总数量一样重要,所以设定一个固定的时间和地点 - 这有助于你克服常见的干扰。下一部分将列出一些示例目标。


· 多样化。 你知道的越多,你变得越有价值。至少,你应该了解你目前工作中特定技术的细节,但不要止步于此。计算机技术变化迅速 - 今天的热门话题可能在明天(或至少不那么受欢迎)变得几乎无用。你掌握的技能越多,你的适应能力就越强。


· 风险管理。 不同的技术均匀地分布在从高风险高回报到低风险低回报的范围内。把所有的钱都投资在高风险的股票上是不明智的,因为它们可能会突然崩盘。同样,你不应该把所有的钱都投资在保守的领域 - 你可能会错过机会。不要把你的技术鸡蛋都放在一个篮子里。


· 低买高卖。 在新兴技术变得流行之前开始学习可能就像寻找被低估的股票一样困难,但回报可能同样好。在Java刚刚发明出来后学习可能是有风险的,但那些早期用户在Java变得流行时获得了可观的回报。


· 重新评估和调整。 这是一个动态的行业。你上个月开始研究的时髦技术可能现在已经降温了。也许你需要刷新一下你很久没有使用过的数据库技术的知识。或者,你可能想尝试一种不同的语言,这可能使你在新的角色中处于更好的位置......


在所有这些指导原则中,下面这个是最简单实施的。


(程序员的软技能:ke.qq.com/course/6034346)


定期在你的知识投资组合中进行投资。


目标。


既然你有了一些指导原则,并知道何时添加什么到你的知识投资组合中,那么获取构成它的智力资产的最佳方法是什么呢?以下是一些建议:


· 每年学习一门新语言。


不同的语言以不同的方式解决相同的问题。学习多种不同的解决方案有助于拓宽你的思维,避免陷入常规模式。此外,由于充足的免费资源,学习多门语言变得更加容易。


· 每月阅读一本技术书籍。


尽管互联网上有大量的短文和偶尔可靠的答案,但要深入理解通常需要阅读更长的书籍。浏览书店页面,选择与你当前项目主题相关的技术书籍。一旦养成这个习惯,每月读一本书。当你掌握了所有当前使用的技术后,扩大你的视野,学习与你的项目无关的东西。


· 也阅读非技术书籍。


请记住,计算机是被人类使用的,而你所做的最终是为了满足人们的需求 - 这是至关重要的。你与人合作,被人雇佣,甚至可能会面临来自人们的批评。不要忘记这个方程式的人类一面,这需要完全不同的技能(通常被称为软技能,听起来可能很容易,但实际上非常具有挑战性)。


· 参加课程。


在当地大学或在线寻找有趣的课程,或者你可能会在下一个商业博览会或技术会议上找到一些课程。


· 加入当地的用户组和论坛。


不要只是作为观众成员;要积极参与。孤立自己对你的职业生涯是有害的;了解你公司之外的人在做什么。


· 尝试不同的环境。


如果你只在Windows上工作,花点时间在Linux上。如果你对简单的编辑器和Makefile感到舒适,尝试使用最新的复杂IDE,反之亦然。


· 保持更新。


关注不同于你当前工作的技术。阅读相关的新闻和技术文章。这是了解使用不同技术的人的经验以及他们使用的特定术语的极好方式,等等。


持续的投资是至关重要的。一旦你熟悉了一门新的语言或技术,继续前进并学习另一门。


无论你是否在项目中使用过这些技术,或者是否应该将它们放在你的简历上,都不重要。学习过程将拓展你的思维,开启新的可能性,并赋予你在处理任务时的新视角。思想的跨领域交流是至关重要的;尝试将你所学应用到你当前的项目中。即使项目不使用特定的技术,你仍然可以借鉴其中的思想。例如,理解面向对象编程可能会导致你编写更具结构的C代码,或者理解函数式编程范 paradigms 可能会影响你如何处理Java等等。


学习机会。


你正在狼吞虎咽地阅读,始终站在你领域的突破前沿(这并不是一项容易的任务)。然而,当有人问你一个问题,你真的不知道的时候,不要停在那里 - 把找到答案当做一个个人挑战。问问你周围的人或在网上搜索 - 不仅在主流圈子中,还要在学术领域中搜索。


如果你自己找不到答案,寻找能够找到答案的人,不要让问题无解地悬而未决。与他人互动有助于你建立你的人际网络,你可能会在这个过程中惊喜地找到解决其他无关问题的方法 - 你现有的知识投资组合将不断扩展。


所有的阅读和研究需要时间,而时间总是不够的。因此,提前准备,确保你在无聊的时候有东西可以阅读。在医院排队等候时,通常会有很好的机会来完成一本书 - 只需记得带上你的电子阅读器。否则,你可能会在医院翻阅旧年鉴,而里面的折叠页来自1973年的巴布亚新几内亚。


批判性思维。


最后一个要点是对你阅读和听到的内容进行批判性思考。你需要确保你投资组合中的知识是准确的,没有受到供应商或媒体炒作的影响。小心狂热的狂热分子,他们认为他们的观点是唯一正确的 - 他们的教条可能不适合你或你的项目。


不要低估商业主义的力量。搜索引擎有时只是优先考虑流行的内容,这并不一定意味着这是你最好的选择;内容提供者也可以支付费用来使他们的材料排名更高。书店有时会将一本书突出地摆放,但这并不意味着它是一本好书,甚至可能不受欢迎 - 这可能只是有人支付了那个位置。


(程序员的软技能:ke.qq.com/course/6034346)


批判性分析你所阅读和听到的内容。


批判性思维本身就是一个完整的学科,我们鼓励你深入研究和学习这门学科。让我们从这里开始,提出一些发人深省的问题。


· 五问“为什么”。


我最喜欢的咨询技术之一是至少连续问五次“为什么”。这意味着在得到一个答案后,你再次问“为什么”。像一个坚持不懈的四岁孩子提问一样重复这个过程,但请记住要比孩子更有礼貌。这样做可以让你更接近根本原因。


· 谁从中受益?


尽管听起来可能有点功利主义,但追踪金钱的流动往往可以帮助你理解潜在的联系。其他人或其他组织的利益可能与你的利益保持一致,也可能不一致。


· 背景是什么?


一切都发生在自己的背景下。这就是为什么声称“解决所有问题”的解决方案通常站不住脚,宣扬“最佳实践”的书籍或文章经不起审查的原因。 “对谁最好?” 是一个需要考虑的问题,以及关于前提条件、后果以及情况是短期还是长期的问题。


· 在何种情况下和何地可以起作用?


在什么情况下?是否已经太晚了?是否还太早了?不要只停留在一阶思维(接下来会发生什么);参与到二阶思维中:接下来会发生什么?


· 为什么这是一个问题?


是否有一个基础模型?这个基础模型是如何工作的?


不幸的是,如今找到简单的答案是具有挑战性的。然而,通过广泛的知识投资组合,并对你遇到的广泛技术出版物进行一些批判性分析,你可以理解那些复杂的答案。


(程序员的软技能:ke.qq.com/course/6034346)


作者:用心看世界Heart
链接:https://juejin.cn/post/7271908000414580776
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

限流:别说算法了,就问你“阈值”怎么算?

基础 限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。 算法 限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间...
继续阅读 »

基础


限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。


算法


限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。



  • 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间它是不会管服务器的真实负载的。

  • 动态算法也叫做自适应限流算法,典型的是 BBR 算法。这一类算法利用一系列指标来判定是否应该减少流量或者放大流量。动态算法和 TCP 的拥塞控制是非常接近的,只不过 TCP 控制的是报文流量,而微服务控制的是请求流量。


令牌桶


系统会以一个恒定的速率产生令牌,这些令牌会放到一个桶里面,每个请求只有拿到了令牌才会被执行。每当一个请求过来的时候,就需要尝试从桶里面拿一个令牌。如果拿到了令牌,那么请求就会被处理;如果没有拿到,那么这个请求就被限流了。


漏桶


漏桶是指当请求以不均匀的速度到达服务器之后,限流器会以固定的速率转交给业务逻辑。


漏桶是绝对均匀的,而令牌桶不是绝对均匀的。


固定窗口与滑动窗口


固定窗口是指在一个固定时间段,只允许执行固定数量的请求。比如说在一秒钟之内只能执行 100 个请求。


滑动窗口类似于固定窗口,也是指在一个固定时间段内,只允许执行固定数量的请求。区别就在于,滑动窗口是平滑地挪动窗口,而不像固定窗口那样突然地挪动窗口。


限流对象


可以是集群限流或者单机限流,也可以是针对具体业务来做限流。


针对业务对象限流,这一类限流对象就非常多样。



  • VIP 用户不限流而普通用户限流。

  • 针对 IP 限流。用户登录或者参与秒杀都可以使用这种限流,比方说设置一秒钟最多只能有 50 个请求,即便考虑到公共 IP 的问题,正常的用户手速也是没那么快的。

  • 针对业务 ID 限流,例如针对用户 ID 进行限流。


限流后的做法



  • 同步阻塞等待一段时间。如果是偶发性地触发了限流,那么稍微阻塞等待一会儿,后面就有极大的概率能得到处理。比如说限流设置为一秒钟 100 个请求,恰好来了 101 个请求。多出来的一个请求只需要等一秒钟,下一秒钟就会被处理。但是要注意控制住超时,也就是说你不能让人无限期地等待下去。

  • 同步转异步。它是指如果一个请求没被限流,那就直接同步处理;而如果被限流了,那么这个请求就会被存储起来,等到业务低峰期的时候再处理。这个其实跟降级差不多。

  • 调整负载均衡算法。如果某个请求被限流了,那么就相当于告诉负载均衡器,应该尽可能少给这个节点发送请求。


亮点


突发流量



漏桶算法非常均匀,但是令牌桶相比之下就没那么均匀。令牌桶本身允许积攒一部分令牌,所以如果有偶发的突发流量,那么这一部分请求也能得到正常处理。但是要小心令牌桶的容量,不能设置太大。不然积攒的令牌太多的话就起不到限流效果了。例如容量设置为 1000,那么要是积攒了 1000 个令牌之后真的突然来了 1000 个请求,它们都能拿到令牌,那么系统可能撑不住这突如其来的 1000 个请求。



请求大小


如果面试官问到为什么使用了限流,系统还是有可能崩溃,或者你在负载均衡里面聊到了请求大小的问题,都可以这样来回答,关键词是请求大小。



限流和负载均衡有点儿像,基本没有考虑请求的资源消耗问题。所以负载均衡不管怎么样,都会有偶发性负载不均衡的问题,限流也是如此。例如即便我将一个实例限制在每秒 100 个请求,但是万一这个 100 个请求都是消耗资源很多的请求,那么最终这个实例也可能会承受不住负载而崩溃。动态限流算法一定程度上能够缓解这个问题,但是也无法根治,因为一个请求只有到它被执行的时候,我们才知道它是不是大请求。



计算阈值


总体上思路有四个:看服务的观测数据、压测、借鉴、手动计算。


看服务的性能数据属于常规解法,基本上就是看业务高峰期的 QPS 来确定整个集群的阈值。如果要确定单机的阈值,那就再除以实例个数。所以你可以这样来回答,关键词是业务性能数据。



我们公司有完善的监控,所以我可以通过观测到的性能数据来确定阈值。比如说观察线上的数据,如果在业务高峰期整个集群的 QPS 都没超过 1000,那么就可以考虑将阈值设定在 1200,多出来的 200 就是余量。 不过这种方式有一个要求,就是服务必须先上线,有了线上的观测数据才能确定阈值。并且,整个阈值很有可能是偏低的。因为业务巅峰并不意味着是集群性能的瓶颈。如果集群本身可以承受每秒 3000 个请求,但是因为业务量不够,每秒只有 1000 个请求,那么我这里预估出来的阈值是显著低于集群真实瓶颈 QPS 的。



压测



不过我个人觉得,最好的方式应该是在线上执行全链路压测,测试出瓶颈。即便不能做全链路压测,也可以考虑模拟线上环境进行压测,再差也应该在测试环境做一个压力测试。



从理论上来说,你可以选择 A、B、C 当中的任何一个点作为你的限流的阈值。


A 是性能最好的点。A 之前 QPS 虽然在上升,但是响应时间稳定不变。在这个时候资源利用率也在提升,所以选择 A 你可以得到最好的性能和较高的资源利用率。


B 是系统快要崩溃的临界点。很多人会选择这个点作为限流的阈值。这个点响应时间已经比较长了,但是系统还能撑住。选择这个点意味着能撑住更高的并发,但是性能不是最好的,吞吐量也不是最高的。


C 是吞吐量最高的点。实际上,有些时候你压测出来的 B 和 C 可能对应到同一个 QPS 的值。选择这个点作为限流阈值,你可以得到最好的吞吐量。


性能 A、并发 B、吞吐量 C。


无法压测:



不过如果真的做不了,或者来不及,或者没资源,那么还可以考虑参考类似服务的阈值。比如说如果 A、B 服务是紧密相关的,也就是通常调用了 A 服务就会调用 B 服务,那么可以用 A 已经确定的阈值作为 B 的阈值。又或者 A 服务到 B 服务之间有一个转化关系。比如说创建订单到支付,会有一个转化率,假如说是 90%,如果创建订单的接口阈值是 100,那么支付的接口就可以设置为 90。



如果我这是一个全新的业务呢?也就是说,你都没得借鉴。这个时候就只剩下最后一招了——手动计算。



实在没办法了,就只能手动计算了。也就是沿着整条调用链路统计出现了多少次数据库查询、多少次微服务调用、多少次第三方中间件访问,如 Redis,Kafka 等。举一个最简单的例子,假如说一个非常简单的服务,整个链路只有一次数据库查询,这是一个会回表的数据库查询,根据公司的平均数据这一次查询会耗时 10ms,那么再增加 10 ms 作为 CPU 计算耗时。也就是说这一个接口预期的响应时间是 20ms。如果一个实例是 4 核,那么就可以简单用 1000ms÷10ms×4=400 得到阈值。




手动计算准确度是很差的。比如说垃圾回收类型语言,还要刨除垃圾回收的开销,相当于 400 打个折扣。折扣多大又取决于你的垃圾回收频率和消耗。



升华:



最好还是把阈值做成可以动态调整的。那么在最开始上线的时候就可以把阈值设置得比较小。后面通过观测发现系统还很健康,就可以继续上调阈值。





此文章为9月Day25学习笔记,内容来源于极客时间《后端工程师的高阶面经》


作者:09cakg86qfjwymvm8cd3h1dew
来源:juejin.cn/post/7282245376425459768
收起阅读 »

有人说SaToken吃相难看,你怎么看。

前言 今天摸鱼逛知乎,偶然看到了一个回答,8月份的,是关于SaToken的,一时好奇就点了进去。 好家伙,因为一个star的问题,提问的人抱怨了许多,我有些意外,就仔细看了下面的评论,想知道一部分人的看法。 案发现场 大体上,分为两派。 一派是...
继续阅读 »

前言



今天摸鱼逛知乎,偶然看到了一个回答,8月份的,是关于SaToken的,一时好奇就点了进去。



1.png



好家伙,因为一个star的问题,提问的人抱怨了许多,我有些意外,就仔细看了下面的评论,想知道一部分人的看法。



案发现场



大体上,分为两派。




一派是对于强制star尤为反感,乃至因爱生恨(打个问号)?




比如下面这种,狂喷作者的。当我看到所谓“花几个工作日自己也能撸一个”这句话的时候,差点没忍住把酱香拿铁喷在电脑上。




本想敲几个字对垒下,但我好歹也是知乎认证的号,想想算了,没必要和这种人打口水仗。



4.png



还有一些是拿数据指责Sa-Token,以及搬出Spring Security做对比的,字里行间一股子微博的味道。



5.png



总而言之,反感这种强制star的人,我发现他们是内心真的极其反感,就像是自己被作者抛弃了一样。



7.png



后面喷着喷着,拔出萝卜带出泥,好吧,ruoyi也被拉出来示众了,这味儿太冲了。



8.png



当然,另一派就是持不同看法的,里面有一句话总结的倒是挺有意思。



6.png



说到这里,其实Sa-Token的作者也亲自下场做了一些解释,比如解释不想star可以如何做,这一点我觉得略显牵强,但后面也给了别的解决方式,听取了部分评论者的中肯意见。



2.png



重要的是,作者最后的回答,就像是无声地呐喊,也许很多喷子接受不了这种呐喊,因为这个“孩子”不是他们的,别人家的孩子跟我有什么关系。



3.png


国内开源现状



通过这个事情,其实勾起了我一些回忆,可能年轻点的程序员是不了解的,国内的开源生态以前是个什么情况。




像我这样年纪稍微大点的可能就见过那个过程,说白了,就是来一批死一批。




没错,国内开源生态就是个充满病菌的牧场,里面养了一群牛羊,结局是大多都病死了,真正能上餐桌的却没几个。




还有人记得当年开源生态圈很离谱的一件事情吗,XXL-JOB的作者发帖伸冤,因为自己的开源项目竟然被某个互联网公司拿去申请了软著。




等于说一个花费心力的项目,仅仅因为开源协议被钻了漏洞,就直接成别人的了,作者没办法只能在网上伸冤求助,以及找开源中国出面解决。




为什么这些公司敢这么做,换成你是作者你接受得了么,你有信心以个人的力量对抗事先有准备的这些打擦边球的侵权么。




因为国内的开源生态就是病态的、畸形的,那几年国内开源项目如雨后春笋,绝大部分作者根本还没有较高的经营意识,凭的就是一腔热爱分享的情怀,以及对拥有自己的一个开源项目这件事的热忱。




然后因为不懂法律,被钻空子,竹篮打水一场空,这样的案例出现一个,就会引起寒蝉效应,开源作者人人自危,谁还敢用授权范围更大的协议。




树上有七只鸟,打死了一只,还剩几只?




然后,再举例说一下上面截图中有喷子提到的ruoyi。




我想问问,现在有多少Java程序员是一路看着ruoyi走过来的。




我猜不多,就算有,也是中途上车的。




我可以简单说下ruoyi当初的处境,虽然只是一个后台管理的项目,我是真没想到时隔多年作者竟然还在写。




当初围绕在ruoyi身边的是一大堆出色的后台管理项目,各具特色,不少都比它要火,但最后具备代表性的只剩ruoyi了。




因为作者一直在迭代,我记得第一次看到ruoyi的时候,作者还写着项目名称的描述,是想象自己未来女儿的名字,所以起了若依。




能坚持这么多年不停歇,那些年你也根本别想凭着开源项目赚什么钱,估计连你工资的零头都没有,但人家还是能迭代到现在。




我就想着,单纯寻思着,也该到了人家收获果实的季节了吧。




我是打心里佩服这些人的,我没觉得比别人差,有些项目花时间我也能写,问题是,我做不到啊,你呢。



总结



如果有一个同行写了开源项目,他想挣钱,我支持,但是项目越来越烂,我会离开,后会无期。




如果有一个同行写了开源项目,他想挣钱,我支持,但是项目越来越好,我会分享,也会付钱。




当我们不断坚持追求,最终换来真正感人的回报,何尝不是生命中最美妙的旋律。




我真诚希望给国内优秀的开源作者更多能挣钱的空间,让那些项目越来越好。




这是我对那些当初“死去”的开源作者的缅怀,也是对未来更多开源作者的殷切期待。




以上纯属个人看法,不收钱的,轻点喷。




如果喜欢,请点赞+关注↑↑↑,持续分享干货和行业动态哦~


作者:程序员济癫
来源:juejin.cn/post/7282696271863906316
收起阅读 »

Nginx +Tomcat 负载均衡,动静分离集群

1. 介绍 通常情况下,一个 Tomcat 站点由于可能出现单点故障及无法应付过多客户复杂多样的请求等情况,不能单独应用于生产环境下,所以我们需要一套更可靠的解决方案Nginx 是一款非常优秀的 http 服务器软件,它能够支持高达 5000 个并发...
继续阅读 »

1. 介绍


  • 通常情况下,一个 Tomcat 站点由于可能出现单点故障及无法应付过多客户复杂多样的请求等情况,不能单独应用于生产环境下,所以我们需要一套更可靠的解决方案
  • Nginx 是一款非常优秀的 http 服务器软件,它能够支持高达 5000 个并发连接数的响应,拥有强大的静态资源处理能力,运行稳定,并且内存、CPU 等系统资源消耗非常低
  • 目前很多大型网站都应用 Nginx 服务器作为后端网站的反向代理及负载均衡器,来提升整个站点的负载并发能力.

小结

  • Nginx是一款非常优秀的HTTP服务器软件

  • 支持高达50 000个并发连接数的响应

  • 拥有强大的静态资源处理能力

  • 运行稳定

  • 内存,CPU等系统资源消耗非常低


1.1. Tomcat重要目录


1.2. 反向代理




 反向代理(Reverse Proxy)方式是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。


反向代理是为服务端服务的,反向代理可以帮助服务器接收来自客户端的请求,帮助服务器做请求转发,负载均衡等。


反向代理对服务端是透明的,对我们是非透明的,即我们并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。


反向代理的优势:


  • 隐藏真实服务器;
  • 负载均衡便于横向扩充后端动态服务;
  • 动静分离,提升系统健壮性。



Nginx配置反向代理的主要参数


  • upstream服务池名{}
    • 配置后端服务器池,以提供响应数据
    1. proxy_pass http://服务池名
    • 配置将访问请求转发给后端服务器池的服务器处理


1.3. 动静分离原理


服务端接收来自客户端的请求中,既有静态资源也有动态资源,静态资源由Nginx提供服务,动态资源Nginx转发至后端


服务端接收来自客户端的请求中,既有动态资源,也有静态资源。静态资源由ngixn提供服务。动态资源由nginx 转发到后端tomcat 服务器。


静态页面一般 有html,htm,css 等路径, 动态页面则一般是jsp ,php 等路径。nginx 在站点的location 中 通过正则,或者 前缀,或者 后缀等方法匹配。当匹配到用户访问路径中有 jsp 时,则转发给后端的处理动态资源的web服务器处理。如果匹配到的路径中有 html 时,则nginx 自己处理。 



1.4. Nginx 静态处理优势

  1. Nginx处理静态页面的效率远高于Tomcat的处理能力
  2. 若Tomcat的请求量为1000次,则Nginx的请求量为6000次
  3. Tomcat每秒的吞吐量为0.6M,Nginx的每秒吞吐量为3 .6M
  4. Nginx处理静态资源的能力是Tomcat处理的6倍

1.5. 吞吐量 / 吞吐率


吞吐量是指系统处理客户请求数量的总和,可以指网络上传输数据包的总和,也可以指业务中客户端与服务器交互数据量的总和。


吞吐率是指单位时间内系统处理客户请求的数量,也就是单位时间内的吞吐量。可以从多个维度衡量吞吐率:①业务角度:单位时间(每秒)的请求数或页面数,即请求数 / 秒或页面数 / 秒;②网络角度:单位时间(每秒)网络中传输的数据包大小,即字节数 / 秒等;③系统角度,单位时间内服务器所承受的压力,即系统的负载能力。


吞吐率(或吞吐量)是一种多维度量的性能指标,它与请求处理所消耗的 CPU、内存、IO 和网络带宽都强相关。


2. Nginx+Tomcat负载均衡、动静分离




1.部署Nginx 负载均衡器

关闭防火墙
systemctl stop firewalld
setenforce 0

安装
yum -y install pcre-devel zlib-devel openssl-devel gcc gcc-c++ make

useradd -M -s /sbin/nologin nginx

cd /opt
tar zxvf nginx-1.12.0.tar.gz -C /opt/

cd nginx-1.12.0/
./configure \
--prefix=/usr/local/nginx \
--user=nginx \
--group=nginx \
--with-file-aio \ #启用文件修改支持
--with-http_stub_status_module \ #启用状态统计
--with-http_gzip_static_module \ #启用 gzip静态压缩
--with-http_flv_module \ #启用 flv模块,提供对 flv 视频的伪流支持
--with-http_ssl_module #启用 SSL模块,提供SSL加密功能
--with-stream

./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --with-file-aio --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-stream

make && make install
ln -s /usr/local/nginx/sbin/nginx /usr/local/sbin/

vim /lib/systemd/system/nginx.service
[Unit]
Description=nginx
After=network.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStart=/usr/local/nginx/sbin/nginx
ExecrReload=/bin/kill -s HUP $MAINPID
ExecrStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target

chmod 754 /lib/systemd/system/nginx.service
systemctl start nginx.service
systemctl enable nginx.service



2.部署2台Tomcat 应用服务器

systemctl stop firewalld
setenforce 0

tar zxvf jdk-8u91-linux-x64.tar.gz -C /usr/local/

vim /etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_91
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:$PATH

source /etc/profile

tar zxvf apache-tomcat-8.5.16.tar.gz

mv /opt/apache-tomcat-8.5.16/ /usr/local/tomcat

/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh

netstat -ntap | grep 8080



3.动静分离配置

(1)Tomcat1 server 配置
mkdir /usr/local/tomcat/webapps/test
vim /usr/local/tomcat/webapps/test/index.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
<head>
<title>JSP test1 page</title> #指定为 test1 页面
</head>
<body>
<% out.println("动态页面 1,http://www.test1.com");%>
</body>
</html>


vim /usr/local/tomcat/conf/server.xml
#由于主机名 name 配置都为 localhost,需要删除前面的 HOST 配置
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">
<Context docBase="/usr/local/tomcat/webapps/test" path="" reloadable="true">
</Context>
</Host>

/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh



4 Nginx server 配置
#准备静态页面和静态图片
echo '<html><body><h1>这是静态页面</h1></body></html>' > /usr/local/nginx/html/index.html
mkdir /usr/local/nginx/html/img
cp /root/game.jpg /usr/local/nginx/html/img

vim /usr/local/nginx/conf/nginx.conf
......
http {
......
#gzip on;

#配置负载均衡的服务器列表,weight参数表示权重,权重越高,被分配到的概率越大
upstream tomcat_server {
server 192.168.85.60:8080 weight=1;
server 192.168.85.70:8080 weight=1;
server 192.168.85.80:8080 weight=1;
}

server {
listen 80;
server_name http://www.wa.com;

charset utf-8;

#access_log logs/host.access.log main;

#配置Nginx处理动态页面请求,将 .jsp文件请求转发到Tomcat 服务器处理
location ~ .*\.jsp$ {
proxy_pass http://tomcat_server;
#设置后端的Web服务器可以获取远程客户端的真实IP
##设定后端的Web服务器接收到的请求访问的主机名(域名或IP、端口),默认HOST的值为proxy_pass指令设置的主机名。如果反向代理服务器不重写该请求头的话,那么后端真实服务器在处理时会认为所有的请求都来自反向代理服务器,如果后端有防攻击策略的话,那么机器就被封掉了。
proxy_set_header HOST $host;
##把$remote_addr赋值给X-Real-IP,来获取源IP
proxy_set_header X-Real-IP $remote_addr;
##在nginx 作为代理服务器时,设置的IP列表,会把经过的机器ip,代理机器ip都记录下来
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

#配置Nginx处理静态图片请求
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ {
root /usr/local/nginx/html/img;
expires 10d;
}

location / {
root html;
index index.html index.htm;
}
......
}
......
}





3. Nginx 负载均衡模式:


  1. rr 负载均衡模式:
  2. 每个请求按时间顺序逐一分配到不同的后端服务器,如果超过了最大失败次数后(max_fails,默认1),在失效时间内(fail_timeout,默认10秒),该节点失效权重变为0,超过失效时间后,则恢复正常,或者全部节点都为down后,那么将所有节点都恢复为有效继续探测,一般来说rr可以根据权重来进行均匀分配。

    1. least_conn 最少连接:

    优先将客户端请求调度到当前连接最少的服务器。

    1. ip_hash 负载均衡模式:

    每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题,但是ip_hash会造成负载不均,有的服务请求接受多,有的服务请求接受少,所以不建议采用ip_hash模式,session 共享问题可用后端服务的 session 共享代替 nginx 的 ip_hash(使用后端服务器自身通过相关机制保持session同步)。

    1. fair(第三方)负载均衡模式:

    按后端服务器的响应时间来分配请求,响应时间短的优先分配。

    1. url_hash(第三方)负载均衡模式:

    基于用户请求的uri做hash。和ip_hash算法类似,是对每个请求按url的hash结果分配,使每个URL定向到同一个后端服务器,但是也会造成分配不均的问题,这种模式后端服务器为缓存时比较好。

Nginx 四层代理配置:
./configure --with-stream

和http同等级:所以一般只在http上面一段设置,
stream {

upstream appserver {
server 192.168.80.100:8080 weight=1;
server 192.168.80.101:8080 weight=1;
server 192.168.80.101:8081 weight=1;
}
server {
listen 8080;
proxy_pass appserver;
}
}

http {
......

7层代理与4层代理区别


总结

  • Nginx 支持哪些类型代理?
    1. 反向代理 代理服务端 7层方代理向代理 4层方向

    2. 正向代理 代理客户端 代理缓存

    3. 7层 基于 http,https,mail 等七层协议的反向代理

    • 使用场景: 动静分离

    • 特点:功能强大,但转发性能较4层偏低

    • 配置: 在http块里设置 upstream 后端服务池: 在seever块里用location匹配动态页面路径,使用 proxy_pass http://服务器池名 进行七层协议(http协议)转发

http {
upstream backersrver [weight= fail= ...]
server IP1: PORT1 [weight= fail= ...]
......
}

server {
listen 80;
server_name XXX;
location ~ 正则表达式 {
proxy_pass http://backeserer;
.......
}
}

}



  1. 4层 基于 IP+(tcp或者udp)端口的代理
  • 使用场景: 负载均衡器 /负载调度器,做服务器集群的访问入口

  • 特点:只能根据IP+端口转发,但转发性能较好

  • 配置: 和http块同一层,一般在http块上面配置

stream {
upstream backerserver {
server IP1:PORT1 [weight= fail= ...]
server IP2:PORT2 [weight= fail= ...]
.....
}

server {
listen 80;
server_name XXX;
proxy_pass backerserver;
}


调度算法 6种


轮询 加权轮询 最少/小连接 ip_hash fair url_hash


会话保持
ip_hash url_hash 可能会导致负载不均衡
通过后端服务器的session共享来实现


Nginx+Tomcat 动静分离

  • Nginx处理静态资源请求,Tomcat处理动态页面请求
  • 怎么实现动态分离

    • Nginx使用location去正则匹配用户的访问路径的前缀或者后缀去判断接受的请求是静态的还是动态的,静态资源请求在Nginx本地进行处理响应,动态页面通过反向代理转发给后端应用服务器

    怎么实现反向代理

    • 先在http块中使用upstream模块定义服务器组名,使用location匹配路径在用porxy_pass http://服务器组名 进行七层转发转发

    反向代理2种类型

    • 基于7层的协议http,HTTPS,mail代理
    • 基于4层的IP+(TCP/UDP)PORT的代理

    4层代理配置

    • 在http块同一层上面配置stream模块,在stream模块中配置upstream模块定义服务器组名和服务器列表,在stream模块中的server模块配置监听的IP:端口,主机名,porxy_pass 服务器组名


Nginx调度策略/负载均衡模式算法6种

 轮询rr    加权轮询weight     最少/小连接least     ip_hash      fair      url_hash    
配置在upstream 模块中

Nginx如何实现会话保持

ip_hash     url_hash    
通过后端服务器session共享
使用stick——cookie——insert基于cookie来判断
通过后端服务器session共享实现

作者:命令加载中
链接:https://juejin.cn/post/7156189527898505253
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你的代码提交友好吗?

Git 是目前世界上最先进的分布式版本控制系统,而针对Git代码提交,我们一般对于记录描述怎么操作的呢?当我是个初入行的码农时,我希望你管我怎么提交,一般就几个字,我功能完成即可,例如:git commit -m "调整修改" 当我开始变为资深码农,并且开始...
继续阅读 »

Git 是目前世界上最先进的分布式版本控制系统,而针对Git代码提交,我们一般对于记录描述怎么操作的呢?当我是个初入行的码农时,我希望你管我怎么提交,一般就几个字,我功能完成即可,例如:

git commit -m "调整修改"

当我开始变为资深码农,并且开始管理整个项目的代码质量以及规范时,看着年轻人提交的代码,你这都是个啥,啥叫调整修改。正如我们看着自己当年写的代码,充满怀疑,这竟然是我写的?


玩笑归玩笑,规范化的提交真是一个好习惯,在工作中一份清晰简介规范的 Commit Message 能让后续代码审查、信息查找、版本回退都更加高效可靠。


那么,快捷工具来了,commitizen/cz-cli


Commit Message标准


标准包含HeaderBodyFooter三个部分.

(): 
// ...

// ...


其中,Header 是必需的,Body 和 Footer 非必须。



  1. Header
    Header 部分只有一行,包括三个字段:type(必需)、scope(可选)、subject(必需)


  • type:用于说明类型。可分以下几种类型
  • scope:用于说明影响的范围,比如数据层、控制层、视图层等等。
  • subject:主题,简短描述。一行

  • Body

对 subject 更详细的描述。


  • Footer

主要是对于issue的关联。


安装


官方意思验证了Node.js 12,14,16版本的Node,而我在18上无任何问题。


在本例中,我们将设置存储库以使用 AngularJS 的提交消息约定,也称为 traditional-changelog。还有其他适配器,例如cz-customizable


  • 首先,确保全局安装 Commitizen CLI 工具:
npm install commitizen -g

  • 接下来,在项目中通过输入以下命令初始化以使用cz-conventional-changelog适配器:
# npm
commitizen init cz-conventional-changelog --save-dev --save-exact

# yarn
commitizen init cz-conventional-changelog --yarn --dev --exact

# pnpm
commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact

注意: 如果要在已经配置过的项目里面覆盖安装,则可以应用强制参数--force。还要了解其它详细信息,只需运行 。commitizen help


上面的命令都干了什么呢:

  • 安装了cz-conventional-changelog适配器模块
  • 将下载配置保存到了package.json
  • 将适配器配置也写入了package.json 
...
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}


针对上面第三点适配器配置,你也可以建立一个.czrc文件,写入:

{
"path": "cz-conventional-changelog"
}

  • 使用
当我们提交代码时,就可以将`git commit`命令替换成`git cz`,或者别名`cz`,`git-cz`等等。


[扩展]在项目中本地安装


上边我们的操作其实可以看到,针对的是自己电脑本地项目,那么如果是多人项目,我们肯定希望每个人都能使用同样的规范,那么可以将命令集成到项目中,那么我们就不能全局安装了:

npm install --save-dev commitizen

在 npm 5.2+ 上,可以使用 npx 初始化适配器:

npx commitizen init cz-conventional-changelog --save-dev --save-exact

对于以前版本的 npm(< 5.2),使用项目内部命令即可:

./node_modules/.bin/commitizen init cz-conventional-changelog --save-dev --save-exact

然后,您可以在package.json文件中添加命令:

  ...
"scripts": {
"commit": "cz"
}

这对所有项目使用人员比较统一化,如果他们想进行提交,他们需要做的就是运行npm run commit


[扩展]通过git commit强制提交


针对项目管理者,我们定了一个规范,但是没法指望别人会严格遵守,所以如何使用 git 挂钩和命令行选项将 Commitizen 合并到现有工作流中。这对项目维护者很有用,确保对不熟悉 Commitizen 的人的贡献强制执行正确的提交格式。


首先确保我们是采用项目中本地集成安装了commitizen,然后可以选取以下两种方式之一.


方法一:传统的 git hooks

针对自己使用,修改以下文件:.git/hooks/prepare-commit-msg

#!/bin/bash
exec < /dev/tty && node_modules/.bin/cz --hook || true

注意: 如果prepare-commit-msg文件是新建的,需要执行权限chmod 777 .git/hooks/prepare-commit-msg,否则:




方法二:husky

对于多用户,我们也可以借助husky来统一提交:

1. 安装husky
npm install husky -D

2. 初始化husky配置
npm pkg set scripts.prepare="husky install"
npm run prepare

3. 添加脚本,我们这边针对提交触发
npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

疑问: commitizen文档对于husky推荐利用package.json添加husky配置,但是我这边不起作用,后边研究一下原因。


注意: 一定慎重同时配置husky和本地git hooks,会重复执行。


全局安装


我们开发过程中,其实针对每个项目初始化适配器,不太友好,其实还可以全局配置。


全局安装commitizencz-conventional-changelog

npm install -g commitizen

npm install -g cz-conventional-changelog

用户目录下创建配置文件(Mac下,Linux下同理):

echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

项目和全局都配置了适配器,将先以本地为主。




VS CODE


vs code中可以使用git-commit-plugin 插件,这里不过多扩展了。


访问原文


你的代码提交友好吗? | DLLCNX的博客


作者:DLLCNX
链接:https://juejin.cn/post/7281532014729609251
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

因为数据库与项目经理引发的一点小争执,保存留念

前言        作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。 项目经理:你改一改嘛🤤 我:哎呀,好麻烦啊,不给你写了一个么😭 项目经理:你那...
继续阅读 »

前言


       作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。



项目经理:你改一改嘛🤤


我:哎呀,好麻烦啊,不给你写了一个么😭


项目经理:你那个我数据库不能维护啊,快改改,乖o(^@^)o


我:😣我不我不,为啥不能维护,我不理解


项目经理:你去试试😣球球了,你去试试😭


(当然没我写的这么肉麻嘞🤣,如有雷同,纯属巧合)





       好了,数据库维护,他从前端页面进入后向页面输入肯定要调用sql,问题来了,以下这种形式sql可以是实现随意添加么(没有主键)
在这里插入图片描述
       我写python的第一反应:这有啥问题么,数据库我会个简单的增删改查,但是我感觉应该有函数可以直接往后加吧(很chun的想法,两种不同的语言怎么可能会一样),于是乎我开始了,漫漫搜索之路(因为回家连不上内网mysql,以下用Oracle代替)


使用insert函数



  • 数据库基本增加操作:insert into table_name (column1, column2, ...) VALUES (value1, value2, ...),这里直接跳过全字段添加,选取单字段添加,本以为他会如下图:


INSERT into wang.gjc_data (a1) values ('a');

在这里插入图片描述



  • 实际上如下图(哪怕是选取单字段也是默认增加一行):
    在这里插入图片描述



       我确实懵了,以前从来没有想过这件事,因为从数据库读取下来很多时候数据第一步就是先转置,感觉有点麻烦吧,因为转置完会出现很多意料之外的情况,但是人家数据库就是这么存的,现在轮到自己建数据库才发现数据库规则可太多了,而且自己上传数据也都是一次上传一行,没遇见过也就没有真正想过数据库在没有主键的情况下可以单单只改一个数据么,但是吧,我头铁啊,python能做到为啥数据库不行,我还是不信,我继续搜




  • 多条一次性插入:INSERT ALL INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...) into table_name(column_name1,column_name2) values (value1,value2)...select * from dual;


INSERT ALL 
INTO table_name (A1,A2) values ('a','b')
INTO table_name (B2,C1) VALUES ('c','d')
select * from dual;

       结果显而易见,肯定不是我所期望的那个场面,如下图:
在这里插入图片描述



       说实话我是真搜不着啥信息,找不到想要的答案就全试一遍,撞到南墙就回头了!所以我决定接下来从update语句下手。





使用update函数


       我想想,update好像无法新增一列,好像还没开始就结束了,但是实际页面肯定需要这个条件,那试试能不能达到自己想要的画面


       因为没有主键,所以我选择直接用update,最后结果与预料的一样,一列全部改变,图下图:


update  GJC_DATA set GJC_DATA.c2= 'c2'


在这里插入图片描述
       然后我就想到了第二范式的概念:第二范式要求在满足第一范式的基础上,非码属性必须完全依赖于候选字,也就是要消除部分依赖。
没有主键形成依赖,不满足第二范式。但是好像就算我加上一列自增主键,也无法用insert插入一个指定位置而不是一次插入一行,但是update是可以实现的,如下图(重新创建一个数据库表):


CREATE TABLE WANG.gjc_data(
id int NOT NULL,
a1 varchar(128),
a2 varchar(128),
a3 varchar(128),
a4 varchar(128),
a5 varchar(128),
b1 varchar(128),
b2 varchar(128),
c1 varchar(128),
c2 varchar(128),
c3 varchar(128),
c4 varchar(128),
c5 varchar(128),
c6 varchar(128),
c7 varchar(128),
PRIMARY KEY(id)
);
create sequence id_zeng_1
start with 1 --以1开始
increment by 1;
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'a','d');
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'b','e');
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'c','f');

在这里插入图片描述


update wang.gjc_data set A1='B'  WHERE id=1;

在这里插入图片描述



       好吧,认清现实了,不过insert一次插入一行,下面直接插一行我python使用的时候早就可以用pandas清空空值,他也无法接受,可能他觉得客户看起来不好看吧,得,那凑活给他改改




  • Oracle数据库


在这里插入图片描述



  • Jupyter读取Oracle


在这里插入图片描述


总结


       到这算是结束了,总结一下,我原以为是我数据库学的不精通,做不到指定位置添加,经过这么一番探索后才发现真的没有这种操作,果然,实践才是检验真理的唯一标准,不遇上这事我还真一直有这个误区,算了,这次被自家人嘲笑就嘲笑了,那也比到时候出差去外面丢人强。



       谨以此文提醒自己,不再犯相同错误,数据库并不可以向excel那样用语句向指定位置插入指定值,更新也是需要设置主键或是一列唯一值去做一个指引;理论知识还是比较薄弱,需要持续加强。



作者:LoveAndProgram
来源:juejin.cn/post/7187287554796814393
收起阅读 »

三个月内遭遇的第二次比特币勒索

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错. 用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消. To recover your lost Dat...
继续阅读 »

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错.



用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消.




To recover your lost Database and avoid leaking it: Send us 0.05 Bitcoin (BTC) to our Bitcoin address 3F4hqV3BRYf9JkPasL8yUPSQ5ks3FF3tS1 and contact us by Email with your Server IP or Domain name and a Proof of Payment. Your Database is downloaded and backed up on our servers. Backups that we have right now: mm_wiki, shuang. If we dont receive your payment in the next 10 Days, we will make your database public or use them otherwise.



(按照今日比特币价格,0.05比特币折合人民币4 248.05元..)


大多时候不使用该服务器上安装的mysql,因而账号和端口皆为默认,密码较简单且常见,为在任何地方navicat也可连接,去掉了ip限制...对方写一个脚本,扫描各段ip地址,用常见的几个账号和密码去"撞库",几千几万个里面,总有一两个能得手.


被窃取备份而后删除的两个库,一个是来搭建该wiki系统,另一个是用来亲测mysql主从同步,详见此篇,价值都不大




实践告诉我们,不要用默认账号,不要用简单密码,要做ip限制。…



  • 登录服务器,登录到mysql:



mysql -u root -p





  • 修改密码:


尝试使用如下语句来修改



set password for 用户名@yourhost = password('新密码');



结果报错;查询得知是最新版本更改了语法,需用



alter user 'root'@'localhost' identified by 'yourpassword';




成功~


但在navicat里,原连接依然有效,而输入最新的密码,反倒是失败



打码部分为本机ip


在服务器执行


-- 查询所有用户


select user from mysql.user;


再执行


select host,user,authentication_string from mysql.user;



user及其后的host组合在一起,才构成一个唯一标识;故而在user表中,可以存在同名的root


使用


alter user 'root'@'%' identified by 'xxxxxx';

注意主机此处应为%


再使用


select host,user,authentication_string from mysql.user;

发现 "root@%" 对应的authentication_string已发生改变;


在navicat中旧密码已失效,需用最新密码才可登录


参考:


mysql 5.7 修改用户密码




关于修改账号,可参考此




这不是第一次遭遇"比特币勒索",在四月份,收到了这么一封邮件:



后来证明这是唬人的假消息,但还是让我学小扎,把Mac的摄像头覆盖了起来..


作者:fliter
来源:juejin.cn/post/7282666367239995392
收起阅读 »

日志打得好,代码差不了

1. 前言 众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。 (本文面向后...
继续阅读 »

1. 前言


众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。


(本文面向后端入门同学或是对日志管理缺乏深入了解的朋友)


2. 注解+手动记录


使用注解@Sl4j来自动创建日志类,再通过log.info()手动记录日志,是一种常见且相对灵活的方法。这样,我们可以在需要的任何地方记录所想要的日志内容。


然而,在实际开发中,这种方法可能会导致大量的日志与业务逻辑代码混杂,增加了代码与日志的耦合度。当需要修改业务逻辑时,往往还需要调整相关的日志,这可能导致重复记录,如请求参数和鉴权信息。


尽管如此,我并不是完全反对这种日志处理方法。其简单性和灵活性是其显著优点。但建议开发者结合其他方法,以减少日志与业务逻辑的耦合。


3. AOP统一处理


首先,我会介绍如何利用AOP自动记录日志以及哪些日志适合用AOP来处理。


3.1 请求详情日志

使用AOP统一处理请求详情日志能够有效地解耦控制层和业务层,这是一个非常高效的策略。以下是一个示例,展示如何使用AOP通知类记录详细的请求信息:


@Slf4j
@Aspect
@Component
public class LoggingAspect {

/* 日志输出被访问的接口url */
@Before("execution(* com.steadon.example.controller.*.*(..))")
public void beforeRequest(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

String httpMethod = request.getMethod();
String remoteAddr = request.getRemoteAddr();
String requestURI = request.getRequestURI();
String queryString = request.getQueryString();
String params = "";

if ("POST".equalsIgnoreCase(httpMethod)) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
params = Arrays.toString(args);
}
}

log.info("Request Info: IP [{}] HTTP_METHOD [{}] URL [{}] QUERY_STRING [{}] PARAMS [{}]",
remoteAddr, httpMethod, requestURI, queryString, params);
}
}

通过这个AOP通知类,我们可以轻松地记录详细的请求信息。在遇到问题时,分析此日志通常可以帮助我们迅速找到原因,例如前端参数传递错误或后端参数接收问题。效果图如下:


image.png


3.2 其他

你可以利用AOP来拦截几乎任何切点,无论是消息队列、Mybatis的Mapper操作,还是特定的循环,都可以织入相应的通知。然而,使用AOP时,我们也必须权衡其成本。


AOP的优势并不仅仅在于其性能或易用性,而是它出色的解耦能力。选择是否使用AOP应基于你的日志和业务逻辑之间的耦合程度。只有当耦合度足够高,需要解耦时,AOP才真正显示其价值。


4. Lark机器人 + 全局异常处理


我相信许多读者都已经熟悉飞书机器人(Lark机器人)。通过全局异常捕获并调用飞书机器人进行告警,这是一种极为有效的业务告警通知方式。你可能会想:“哦,这个我知道。” 但是,具体如何实现呢?其实操作起来并不复杂,接下来我会详细介绍:


4.1 创建告警群聊

对于简单的报警,我们通常选择创建群聊机器人,而不是单独的机器人应用。这样做的好处是,我们可以动态管理需要接收告警的开发团队成员。


4.2 创建自定义机器人


  1. 在群聊中,依次选择:设置 -> 群机器人 -> 添加机器人。


image.png



  1. 在机器人详情页面,获取webhook地址。请确保此地址保密。


image.png



  1. 接下来,在代码中集成飞书机器人的告警功能。下面是如何在全局异常处理中集成飞书机器人的示例代码:


/* 飞书机器人告警 */
public String sendLarkNotification(String webhookUrl, String user, String title, String messageBody) throws Exception {
URL url = new URL(webhookUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
ObjectNode content = root.putObject("content").putObject("post").putObject("zh_cn");
content.put("title", title);

ArrayNode contentArray = content.putArray("content");
ArrayNode atUserArray = contentArray.addArray();
atUserArray.addObject().put("tag", "at").put("user_id", user);

ArrayNode messageArray = contentArray.addArray();
messageArray.addObject().put("tag", "text").put("text", messageBody);

root.put("msg_type", "post");

byte[] input = mapper.writeValueAsBytes(root);

try (OutputStream os = connection.getOutputStream()) {
os.write(input, 0, input.length);
}

try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}

代码相对简单,主要逻辑是通过HTTP请求向webhook地址发送带有告警信息的POST请求。为了实现这个请求,我们需要在全局异常处理中调用上述方法。为了便于展示,我直接在全局异常处理类中编写了这个方法。但读者可以考虑创建一个专门的工具类来实现这一功能。接下来,我将展示我的全局异常处理类的逻辑:


@ControllerAdvice
public class GlobalExceptionHandler {

@Value("${notifications.larkBotEnabled}")
private boolean larkBotEnabled;

@ExceptionHandler(value = Exception.class)
public CommonResult<String> handleException(Exception e) {
if (!larkBotEnabled) return CommonResult.fail();
// Send notification to Lark
String title = "线上BUG通报";
String user = "all";
String webhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/f4150b6c-xxxx-xxxx-xxxx-xxxx-xxxx";

try {
String s = sendLarkNotification(webhookUrl, user, title, e.getMessage());
return CommonResult.fail(s);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

你可能会对larkBotEnabled感到好奇。实际上,全局异常捕获并不会自动区分线上环境与开发环境。为了能够在不同的环境中控制是否发送告警,我们使用了这个变量。通过在线上和本地的配置文件中设置不同的值,我们可以轻松地区分这两种环境。


application.yml


spring:
profiles:
active: dev #决定是否使用本地配置
application:
name: app-name

notifications:
larkBotEnabled: true #自定义字段控制报警行为

application-dev.yml


notifications:
larkBotEnabled: false

这样,我们就可以根据不同的环境来决定是否发送告警。以下是告警效果的示例:


image.png


这种告警方式既简洁又直观,帮助我们迅速定位问题。当然,针对不同的问题,我们还可以进一步对告警进行分级处理。


总结:在大型项目中,日志体系的构建需要大量的时间和资源。虽然在实际业务中,我们可能会使用更复杂的日志系统和告警机制,但本文提供的方法对于日常开发已经足够使用。如有任何建议或纠正,欢迎在评论区提出!


作者:雾山小落
来源:juejin.cn/post/7280864416554500131
收起阅读 »

盘点那些国际知名黑客(上篇)

iOS
电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。黑帽黑客...
继续阅读 »

电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。

  • 黑帽黑客以“利欲”为目标,通过破解、入侵去获取不法利益或发泄负面情绪。
    • 灰帽黑客以“昭告”为目标,透过破解、入侵炫耀自己所拥有的高超技术。
    • 白帽黑客以“改善”为目标,破解某个程序作出修改,透过入侵去提醒设备的系统管理者其安全漏洞,有时甚至主动予以修补。


白帽黑客大多是电脑安全公司的雇员,抑或响应招测单位的悬赏,通常是在合法的情况下攻击某系统,而黑帽黑客同时也被称作“Cracker”(溃客),打着黑客的旗帜做不光彩的事情。接下来我们为大家介绍一下世界上非常厉害的顶级黑客。


“互联网之子”亚伦·斯沃茨 



2013 年 1 月 11 日,年仅26 岁的互联网奇才亚伦斯沃茨自杀身亡。他的一生都在为互联网的信息自由而努力。亚伦·斯沃茨 被称作计算机天才、互联网时代的普罗米修斯。但在这些光环的背后,是美国政府为他定下的 13 项重罪指控和最高 35 年的监禁。



1986年亚伦出生在一个程序员之家。3岁学会编程,12岁创建了一个知识共享网站,叫做 The info,功能和维基百科一样,但比维基百科早了 5 年。15岁参与制订了CC协议。18岁入学斯坦福,20岁辍学创业与Reddit项目的两位创始人合伙开公司,并创建了Reddit网站。 Reddit在当时的影响力不断扩大,成为最受欢迎的网站之一。后来,雅伦卖掉Reddit网站,赚了 100 万美元,在他 20 岁那年成为百万富翁。



亚伦参与构建了RSS,这是博客时代的工具,能让用户订阅自己感兴趣的博客,当订阅更新的时候,用户会收到邮件提醒。彼时的亚伦沉浸在互联网程序世界的理想主义美梦里,他希望自己能像他的偶像万维网的发明人蒂姆·博纳斯·李那样,让互联网回归自由、共享的初心。亚伦对赚钱并不感兴趣,他的梦想是追求一个更宏大的目标——互联网知识的自由和共享。



一次机会,亚纶了解到一个名为PACER的网站,它是一个存放法庭电子记录的系统,每看一页里面的内容,联邦政府需要收取 8 美分的管理费用。这项业务每年能带给政府超过 100亿美元的收入。亚纶认为,这些联邦法庭记录的材料本就属于公众,应当免费向公众开放。于是他编写了一个程序,抓取了超过 2000 万页的PACER资料,并将它们投放到公共资源网上,供大家免费阅读,这一举动相当于直接减少了美国司法系统200万美元的收入,PACER也在巨大的舆论压力下逐渐免费。



亚伦有一个“开放图书馆”的梦想,他认为实体的图书馆限制了知识的传播,而互联网是连接书籍、读者、作者、纸张与思想最好的载体。他在08年发表的《开放获取游击队宣言》中写道:信息就是力量,但就像所有力量一样,有些人只想将其占为己有。世界上大多数的期刊都被类似Elsevier、JSTOR这样的巨头垄断,每阅读一篇文献都需要支付一定数量的费用。亚伦想帮助更多的人平等地享受这些知识。于是他通过自己高超的黑客技术,利用麻省理工学院的校园网络免费端口从JSTOR下载了 480 万篇论文,相当于整个文献数据库的80% 。



亚伦毫无意外地被警察逮捕,但由于并未用论文牟利,JSTOR放弃了对他的指控。但马萨诸塞州检察长坚持起诉雅伦违反了1986年的计算机欺诈与滥用法。若罪名成立,亚伦将面临35年的监禁和100万美元的巨额罚款。亚伦拒绝认罪他选择与美国政府斗争。在这期间,他积极参与到各种推动知识共享的运动中,传播他关于知识共享的理念。



2012 年9月 12 日,联邦检察官提出了一份替换起诉书,增加了电子欺诈、非经授权访问计算机等罪名,从原来的 4 项重罪指控变成了 13 项。2013 年1月,雅伦在布鲁克林的公寓中上吊自杀,结束了自己的生命。这一年,他26岁。他死后,超过5万人在白宫网站上请愿,要是起诉亚伦的检察官辞职,维基百科以黑屏为他悼念。



亚伦认为知识共享能提高全人类的智慧,信息共享、言论自由才是真正的平等。在他死后,黑客入侵了麻省理工官网,抗议这个被视为黑客起源地的学府对于亚伦的无所作为。麻省理工的标题页被改为亚伦在2008 年写下的《开放获取游击队宣言》宣言中鼓励每一个网络用户行动起来,阻止商人与政客将网络私有化。



2013 年3月,亚伦被追授詹姆斯麦迪逊奖,用以表彰他捍卫公众的知情权所作出的贡献。


“世界头号黑客”凯文·米特尼克



凯文·米特尼克曾说:“巡游五角大楼,登录克里姆林宫,进出全球所有计算机系统,摧垮全球金融秩序和重建新的世界格局,谁也阻挡不了我们的进攻,我们才是世界的主宰。”



如果说谁的人生像小说一样精彩,那一定当属凯文·米特尼克。他出生于美国洛杉矶,是第一个被美国联邦调查局通缉的黑客,号称“世界头号黑客”。



20世纪80年代,他因多次入侵美国联邦调查局的中央电脑系统等而被逮捕三次。米特尼克的所作所为与人们所熟知的犯罪不同,他所做的一切似乎都不是为了钱,他曾破坏了40多家的安全系统,只是为了表明他“有能力做到”。



2000年,米特改邪归正,成为了一名白帽黑客,成功创办了米特尼克安全咨询公司,专门世界500强企业做网络咨询工作。2023年7月16日去世,享年59岁。


“C语言之父”丹尼斯·里奇



“丹尼斯·里奇一点也不家喻户晓,但是如果你有一台显微镜,能在电脑里看到他的作品,你会发现里面到处都是他的作品。”



丹尼斯·里奇(Dennis Ritchie)是美国计算机科学家,被称为“C语言之父”“Unix之父”。20世纪60年代,丹尼斯·里奇和肯·汤普逊参与了贝尔实验室Multics系统的开发。在开发期间,肯·汤普逊开发了游戏【空间旅行】,但当时的系统不给力,游戏运行速度很慢。



然而不久之后贝尔实验室撤出了Multics计划,里奇和汤普逊利用一台旧的迷你计算机Digital PDP-7,1969年的圣诞节Unix系统诞生了。最初的Unix内核使用B语言编写,为了更好开发Unix,1973年,里奇以B语言为基础发展出C语言,在它的主体设计完成后,他和汤普森又用它完全重写了Unix。



随着计算机的发展,编程语言层出不穷,但无论如何翻涌,都无法改变C语言在编程界德高望重的地位,C++、Java、C#都是在C语言的基础上衍生出来的。而如今诸多流行的操作系统也是在Unix的基础上开发的,如Linux、MacOS甚至最流行的手机系统Android。


丹尼斯·里奇发明的C语言联合Unix操作系统,构建了当代计算机世界的钢筋水泥。正是因为C语言和Unix系统这两项成就,里奇成为了许多编程爱好者膜拜的对象。


“Linux之父”林纳斯·托瓦兹



“Given enough eyeballs,all bugs are shallow.”【很多双眼睛盯着的代码,bug无处藏身】


1991年Linus开发了Linux操作系统,在最初几年里,Linux并没有得到太多关注。但随着互联网的普及,如今的linux已经成为全球最受欢迎的操作系统之一,被广泛应用于服务器、移动设备、家庭电脑和超级计算机等领域。



Linux的诞生充满了偶然,林纳斯经常用他的终端仿真器去访问大学主机上的新闻组和邮件,为了方便读写和下载文件,他自己编写了磁盘驱动程序和文件系统。这些在后来成为了Linux第一个内核的雏形,那时的他年仅21岁。



我们能够看到如今日渐壮大的Linux,但也不难发现,在成功的Linux背后,有着几十年如一日的持之以恒,有着对高质量代码的坚持,更是有着合作的。林纳斯没有建立组织,仅仅通过吸引全球数以万计的自由开发者免费贡献就完成了项目。Linux不仅仅是一个代码项目,也是一种互联网出现以后的新的协作方式——开源模式。


写在最后


现在国家很重视网络安全建设,网络安全已经成为了很多高校的一级学科,因此通过正常学习即可进入网络安全行业,大家一定要遵纪守法,效仿黑客们的行为做一些非法的黑客攻击行为,下期我们将继续为大家送上其他几位世界著名黑客的传奇故事,请大家保持关注哦。


作者:禅道程序猿
链接:https://juejin.cn/post/7273125596951478284
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

智能门锁临时密码的简单实现~~

引子 话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。 某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。” 原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你...
继续阅读 »

引子


话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。


某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。”


原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你。”


“好嘞,那到时候联系”


挂断电话,我随手打开手机上的花粉生活APP,但是感觉有点不对劲,我去,设备咋都离线了(后来发现是网络欠费)?我顿时虎躯一震,脑海中浮现了快递小哥到了后发现自己白跑一趟,带着满头大汗、气喘吁吁并且嘴里一顿C语言输出的尴尬场景...


但是我惊喜的发现,门锁的卡片虽然离线但还可以正常进入,我抱着试一试的心态点进去,临时密码竟然可以正常生成,真牛!


于是我点击了生成临时密码...


电话又响起:“哥我到了,把密码给我吧”


我将临时密码给小哥开了门,一切顺利...




实现


这是前段时间亲身经历的一件事,原本以为智能门锁临时密码的功能需要网络支持,服务器生成临时密码给用户,同时下发到门锁里面。现在发现,并不需要门锁联网也可以执行密码验证的操作。
脑海中思考了下,临时密码离线验证这个功能可能是类似这样实现的:



  • 门锁端和服务器端采用相同的规则生成临时密码,并且密码生成规则里面包含了时间这个因素

  • 用户请求临时密码,服务端按照规则生成临时密码返回给用户

  • 用户输入临时密码解锁,门锁按照同样的规则进行校验
    以上实现是一个直觉性的思考,实际编码落地根据不同的需求会有更多的考虑,以我在使用的遥遥领先牌智能门锁Pro为例,下面来做一个简单的实现...


首先,让来看看这款门锁的临时密码有哪些限制条件:


limit12.png


lim22.png


限制条件有:



  • 单个密码有效期为30分钟

  • 有效期内只能使用一次

  • 一分钟内只能添加一个临时密码


根据这些限制条件和前面的思考,密码生成规则可以这样设置:



  • 拼接产品序列号+当前时间字符串,获取拼接后字符串的hashcode,然后对1000000(百万)取余,得到6位数字作为临时密码。并且时间字符串按照yyyy-MM-dd HH:mm 格式,精确到分钟

  • 加入产品序列号的原因是为了让不同门锁在相同时间产生不同的密码,如果只以时间为变量肯定是不安全的

  • 由于门锁生成的限制条件里面约定了一分钟只能添加一个临时密码,因此时间变量也精确到分钟,保证每分钟的临时密码不同,分钟内相同。


然后是实现思路:



  • 用户请求服务端,服务端根据密码生成规则返回一个临时密码

  • 快递小哥拿着临时密码在门锁现场输入

  • 门锁按照临时密码输入的时间点,计算时间点前30分内每一分钟对应的密码,30分钟对应30个临时密码。为什么是30分钟?因为密码30分钟内有效

  • 门锁将快递小哥输入的密码与生成的30个密码进行一一比对,如果有匹配的密码,说明临时密码有效

  • 将输入的临时密码缓存,每次输入密码时都要去缓存里面判断临时密码是否在30分钟内使用过,如果使用过就不能开锁。为什么要判断是否30分钟内使用过?因为有效期内只能使用一次




有了以上思路,下面代码的编写工作就比较简单了,开整...


首先创建三个类:OtherTerminal、SmartLock、PasswordUtils 分别,表示其他可获取密码的终端、门锁以及跟密码相关的工具类


首先是OtherTerminal类,相当于可获取密码的终端,例如我们的手机或者平板,主要功能是调用PasswordUtils工具类根据门锁的序列号和当前时间来获取有效临时密码。



public class OtherTerminal {
private final static String serialNumber = "XiaoHuaSmartLock001";
public static void main(String[] args) {
System.out.println("当前开锁密码:"+PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(LocalDateTime.now())));
}
}


接着是SmartLock类


SmartLock的main方法里面等待控制台的输入,并对输入的密码进行验证。验证调用了verify方法。


verify方法的执行逻辑:调用PasswordUtils工具类,获取过去30分钟内每分钟对应的临时密码,判断输入的密码是否在这些临时密码当中。如果存在说明临时密码有效,还需对当前密码在过去30分钟内是否使用进行判断,保证密码只能使用一次。这个判断是通过调用PasswordUtils工具类的getAndSet方法实现的。


如果认证成功,则开锁。否则开锁失败。


// 智能门锁
public class SmartLock {

private final static String serialNumber = "XiaoHuaSmartLock001";
private final static Integer expirationTime = 30;


public static void main(String[] args) {
// 步骤:首先生成过去30分钟内的所有数字

Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
int password = scanner.nextInt();
if (verify(password)) {
System.out.println("开锁成功,当前时间:" + LocalDateTime.now());
} else {
System.out.println("开锁失败,当前时间:" + LocalDateTime.now());
}
}
scanner.close();

}

private static boolean verify(Integer inputPassword) {
// 获取当前时间点以前30分钟内的所有密码
LocalDateTime now = LocalDateTime.now();
LocalDateTime validityPeriod = now.minusMinutes(expirationTime);
List<Integer> validityPeriodPasswords = new ArrayList<>();

while (validityPeriod.isBefore(now.plusMinutes(1L))) {
validityPeriodPasswords.add(PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(validityPeriod)));
validityPeriod = validityPeriod.plusMinutes(1L);
}
System.out.println(validityPeriodPasswords);
return validityPeriodPasswords.contains(inputPassword) && PasswordUtils.getAndSet(inputPassword);
}
}

再来看下PasswordUtils工具类,这个类内容较多,分步解释:
首先是生成6位临时密码的generate方法,比较简单。但是这样生成的密码不能以0开头,是缺点!


/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

接着是一个格式化时间的方法,将时间格式化为:yyyy-MM-dd HH:mm。精确到分钟,generate方法的第二个参数time需要调用此方法来保证时间以分钟为单位,这样分钟内生成的密码都是相同的


public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

最后是门锁对临时密码的管理:



  • 临时密码存储在一个map对象中:usedPasswordMap

  • 有一个标记对象clearTag用于标记是否应当对usedPasswordMap进行清理操作,用于清理已过期的临时密码

  • 临时密码存在时间大于30分钟,判断为已过期


下面是临时密码过期判断和过期清理的方法


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval = switch (PasswordUtils.expirationUnit) {
case SECONDS -> duration.toSeconds();
case MINUTES -> duration.toMinutes();
case HOURS -> duration.toHours();
case DAYS -> duration.toDays();
default -> throw new IllegalArgumentException("输入时间类型不支持");
};
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

getAndSet方法:



  • 首先判断是否达到了清理阈值,从而执行是否清理的操作,用于节省资源消耗

  • 从usedPasswordMap中获取当前输入密码是否存在,如果不存在说明密码未使用过,则将当前密码设置到map里面并返回true,否则还要进行进一步的判断,因为可能存在历史密码但是已过期和当前密码重复的情况

  • 若usedPasswordMap中存在当前密码,调用expired方法,如果历史密码过期了说明当前密码有效,并刷新时间戳,否则说明有效期内当前密码已经使用过一次


/**
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}



验证


我将门锁程序部署到我的服务器上面,并运行。随便输入一个数字,例如123456,返回开锁失败。


image.png


然后本地运行OtherTerminal类获取临时密码:974971


image.png
再去门锁上验证试试:开锁成功!


image.png


最后完整的PasswordUtil工具类的代码贴在这里:


// 密码工具类

public class PasswordUtils {
private static Map<Integer, Long> usedPasswordMap = new HashMap<>();
private final static Integer expirationTime = 30;
private final static TimeUnit expirationUnit = TimeUnit.MINUTES;
private final static Integer clearThreshold = 30;
private static Integer clearTag = 0;

/**
* 获取code状态,并设置到使用code里面
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval;
switch (PasswordUtils.expirationUnit) {
case SECONDS:
actualInterval = duration.toSeconds();
break;
case MINUTES:
actualInterval = duration.toMinutes();
break;
case HOURS:
actualInterval = duration.toHours();
break;
case DAYS:
actualInterval = duration.toDays();
break;
default:
throw new IllegalArgumentException("输入时间类型不支持");
}
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

}

最后的最后,这种方法生成的密码有个bug,就是30分钟内生成的30个密码里面会有重复的可能性,不过想来发生概率很低,看后续如何优化了。


作者:持剑的青年
来源:juejin.cn/post/7280459667129188387
收起阅读 »

外甥女问我什么是代码洁癖,我是这么回答的...

1. 引言 哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。 今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。 1.1...
继续阅读 »

1. 引言


哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。


今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。


1.1 背景


代码开发:



一个月后:



后面有时间了改一改吧(放心,不会有时间的,有时间了也不会改)。


六个月后:



如上,是任何一个开发者都会经历的场景:早期的代码根本不能回顾,不然一定会陷入深深的怀疑,这么烂的代码真是出自自己的手吗?


更何况,目前大部分系统都是协同开发,每个程序员的命名规范、编码习惯都不尽相同,就导致了一个系统代码,多个味道的情况。


重构是什么


妍妍:嘿,舅舅,听说你要分享重构,这又是什么新鲜事?



❤:嗨,妍妍!重构就是改进既有代码的设计,让它更好懂、更容易维护,而不改变它的功能。想象一下,它就像是给代码来了个变美的化妆术,但内在还是那个代码,不会变成"不认识的人"。


为什么要重构


露露:哇,听起来好厉害,那为什么我们要重构呢?



❤:哈哈,好问题,露露!因为代码是活的,一天天在变大,当代码变得难以理解、难以修改时,它就像是一头头重的大象,拖慢了我们前进的步伐。重构就像是给大象减肥,使它更轻盈、更灵活,开发速度也能提升不少!


这和你们有小洁癖,爱收拾房间一样,有代码洁癖的程序员也会经常重构 Ta 们的代码呢!


什么时候要重构


妍妍:听起来有道理,但什么时候才应该使用重构呢?



❤:好问题,妍妍!有以下几种情况:




  • 当你看到代码中有好几处长得一模一样的代码,这时候可以考虑把它们合并成一个,减少冗余。




  • 当你的函数或方法看上去比词典还厚重时,可以把它拆成一些小的部分,更好地理解。




  • 当你要修复一个 bug,但却发现原来的代码结构太复杂,修复变得像解迷一样难时,先重构再修复就是个好主意。




  • 当你要添加新功能,但代码不让你轻松扩展时,也可以先重构,然后再扩展。




重构的步骤


露露:明白了舅舅,那重构的具体步骤是什么呢?



❤:问得好,露露,看来你有认真在思考!接下来让我给你介绍一下重构的基本步骤吧!


2. 如何重构


重构之前,我们需要识别出代码里面的坏味道代码。


所谓坏味道,就是指代码的表面的混乱,和深层次的腐化现象。简单来说,就是感觉不太对劲的代码。


2.1 坏味道代码



在《重构-改善既有代码的设计》一书中,讲述了这二十多种坏味道情况,我们下面将挑选最常见的几种来介绍。


1)方法过长


方法过长是指在一个方法里面做了太多的工作,常常伴随着方法中的语句不在同一个抽象层级,比如 dto 和 service 层代码混合在一起,即逻辑分散。


除此之外,方法过长还容易带来一些额外的问题。


问题1:过多的注释


方法太长会导致逻辑难以理解,需要大量的注释,如果 10 行代码需要 20 行注释,代码很难阅读。特别是读代码的时候,常常需要记住大量的上下文。


问题2:面向过程


面向过程的问题在于当逻辑复杂以后,代码会很难维护。


相反地,我们在代码开发时常常用面向对象的设计思想,即把事物抽象成具有共同特征的对象。


解决思路


解决方法过长时,我们遵循这样一条原则:每当感觉要写注释来说明代码时,就把这部分代码写进一个独立的方法里,并根据这段代码的意图来命名。



方法命名原则:可以概括要做的事,而非怎么做。



2)过大的类


一个类做了太多的事情,比如一个类的实现既包含商品逻辑,又包含订单逻辑。在创建时就会出现太多的实例变量和方法,难以管理。


除此之外,过大的类还容易带来两个问题。


问题1:冗余重复


当一个类里面包含两个模块的逻辑时,两个模块容易产生依赖。这在代码编写的过程中,很容易发生 “你带着我,我看着你” 的问题。


即在两个模块中,都看到了和另一个模块相关的程序结构或相同意图的方法。


问题2:耦合结构不良


当类的命名不足以描述所做的事情时,大概率产生了耦合结构不良的问题,这和我们想要编写 “高内聚,低耦合” 的代码目标相悖而行了。


解决思路


将大类根据业务逻辑拆分成小类,如果两个类之间有依赖,则通过外键等方式关联。当出现重复代码时,尽量合并提出来,程序会变得更简洁可维护。


3)逻辑分散


逻辑分散是由于代码架构层次或者对象层次上有不合理的依赖,通常会导致两个问题:


发散式变化


某个类经常因为不同的原因,在不同的方向上修改。


散弹式修改


发生某种变化时,需要多个类中修改。


4)其它坏味道


数据泥团


数据泥团是指很多数据项混乱地融合在一起,不易复用和扩展。


当许多数据项总是一起出现,并且一起出现时更容易分类。我们就可以考虑将数据按业务封装成数据对象。反例如下:


func AddUser(age int, gender, firstName, lastName string) {}

重构之后:


type AddUserRequest struct {
   Age int
   Gender string
   FirstName string
   LastName string
}
func AddUser(req AddUserRequest) {}

基本类型偏执


在大多数高级编程语言里面,都有基本类型和结构类型。在 Go 语言里面,基本类型就是 int、string、bool 等。


基本类型偏执是指我们在定义对象的变量时,常常不考虑变量的实际业务含义,直接使用基本类型。


反例如下:


type QueryMessage struct {
Role        int         `json:"role"`
Content  string    `json:"content"`
}

重构之后:


// 定义对话角色类型
type MessageRole int

const (
HUMAN     MessageRole = 0
ASSISTANT MessageRole = 1
)

type QueryMessage struct {
Role        MessageRole   `json:"role"`
Content  string               `json:"content"`
}

这是 ChatGPT 问答时的请求字段,我们可以看到对话角色为 int 类型,且 0 表示人类,1 表示聊天助手。


当直接使用 int 来表示对话 Role 时,没办法直接从定义里知道更多信息。


但是用 type MessageRole int 定义后,我们就可以根据常量值很清晰地看出对话角色分为两种:HUMAN & ASSISTANT.


混乱的代码层次调用


我们一般的系统都会根据业务 service、中转控制 controller 和数据库访问 dao 等进行分层。一般 controller 调用 service,service 调用 dao。


如果我们在 controller 直接调用 dao,或者 dao 调用 controller,就会出现层次混乱的问题,就可以进行优化了。


5)坏味道带来的问题


妍妍:舅舅,这些坏味道都需要解决吗,你说的这些坏味道代码会带来什么样的影响呢?


❤:是的,代码里如果坏味道代码太多,会带来四个 “难以”



  • 难以理解:新来的开发同学压根看不懂看人的代码,一个模块看了两个周还不知道啥意思。或许不是开发者的水平不够,可能是代码写的太一言难尽。



  • 难以复用:要么是读都读不懂,或者勉强读懂了却不敢用,担心有什么暗坑。或者系统耦合性严重,难以分离可重用部分。



  • 难以变化:牵一发而动全身,即散弹式修改。动了一处代码,整个模块都快没了。




  • 难以测试:改了不好测,难以进行功能验证。命名杂乱,结构混乱,在测试时可能测出新的问题。




3. 重构技巧


露露:哦,原来是这样啊,那我们可以去除它们吗?


❤:当然可以了!就像你们爱收拾房间一样,每一个有责任心(代码洁癖)的程序员,都会考虑代码重构。


而对于重构问题,业界已经有比较好的思路:通过持续不断地重构将代码中的 "坏味道" 清除掉。


1)命名规范


一个好的命名规范应该符合:



  • 精准描述所做的事情

  • 格式符合通用惯例


约定俗成的惯例


我们拿华为公司内部的 Go 语言的开发规范来举例:


场景约束示例
项目名全部小写,多个单词时用中划线 '-' 分隔user-order
包名全部小写,多个单词时用中划线 '-' 分隔config-sit
结构体名首字母大写Student
接口采用 Restful API 的命名方式,路径最后一部分是资源名词如 [get] api/v1/student
常量名首字母大写,驼峰命名CacheExpiredTime
变量名首字母小写,驼峰命名userName,password

2)重构手法


妍妍:哇,这么多成熟的规范可以用啊!那除了规范,我们还需要注意什么吗?


❤:好问题妍妍!接下来我还会介绍一些常见的重构手法:




  • 提取函数:将一个长长的函数分成小块,更容易理解和复用。




  • 改名字:给变量、函数、类等改个名字,更有意义。




  • 消除冗余:找到相似的代码块,合并它们,减少重复。




  • 搬家:把函数或字段移到更合适的地方,让代码更井然有序。




  • 抽象通用类:把通用功能抽出来,变成一个类,增加代码的可重用性。




  • 引入参数对象:当变量过多时,传入对象,消除数据泥团。




  • 使用卫语句:减少 else 的使用,让代码结构更加清晰。




4. 小结


露露:舅舅,你讲得太有趣了,我感觉我也会重构了!


❤:露露真棒,我相信你!重构的思想无处不在,就像生活中都应该留白一样,你们的人生也会非常精彩的。在编程里,重构可以让代码更美观、更容易读懂,提高开发效率,是程序员都应该掌握的技能。


妍妍:我也会了,我也会了!以后我也要写代码,做代码重构,我还要给舅舅的文章点赞。



❤:哈哈哈,好哒,你们都很棒!就像你们喜欢打扫卫生,爱好画画读诗一样,如果以后你们想写代码,它们也会十分的干净整洁,充满诗情画意。



最后,如果你觉得有所收获,别忘了点赞和在看,让更多的人了解重构的神奇之处,一起进步,一起写出更好的代码!


希望这篇文章对你有所帮助,也希望你能在编程的路上越走越远。感谢大家的支持,我们下次再见!🚀✨


最后


妍妍说:看完的你还不赶紧分享、点赞、加入在看吗?



作者:xin猿意码
来源:juejin.cn/post/7277836718760771636
收起阅读 »

全方位对比 Postgres 和 MySQL (2023 版)

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。 随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。 如果看安装数量,MySQL...
继续阅读 »

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。




随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。


如果看安装数量,MySQL 可能仍是全球最大的开源数据库。




Postgres 则自诩为全球最先进的开源关系型数据库。




因为需要与各种数据库及其衍生产品集成,Bytebase 和各种数据库密切合作,而托管 MySQL 和 Postgres 最大的云服务之一 Google Cloud SQL 也是 Bytebase 创始人的杰作之一。


我们对 Postgres 和 MySQL 在以下几个维度进行了比较:


  • 许可证 License
  • 性能 Performance
  • 功能 Features
  • 可扩展性 Extensibility
  • 易用性 Usability
  • 连接模型 Connection Model
  • 生态 Ecosystem
  • 可运维性 Operability



除非另有说明,下文基于最新的主要版本 Postgres 15 和 MySQL 8.0 (使用 InnoDB)。在文章中,我们使用 Postgres 而不是 PostgreSQL,尽管 PostgreSQL 才是官方名称,但被认为是一个错误的决定




许可证 License


  • MySQL 社区版采用 GPL 许可证。
  • Postgres 发布在 PostgreSQL 许可下,是一种类似于 BSD 或 MIT 的自由开源许可。

即便 MySQL 采用了 GPL,仍有人担心 MySQL 归 Oracle 所有,这也是为什么 MariaDB 从 MySQL 分叉出来。


性能 Performance


对于大多数工作负载来说,Postgres 和 MySQL 的性能相当,最多只有 30% 的差异。无论选择哪个数据库,如果查询缺少索引,则可能导致 x10 ~ x1000 的降级。
话虽如此,在极端的写入密集型工作负载方面,MySQL 确实比 Postgres 更具优势。可以参考下文了解更多:



除非你的业务达到了 Uber 的规模,否则纯粹的数据库性能不是决定因素。像 Instagram, Notion 这样的公司也能够在超大规模下使用 Postgres。


功能 Features


对象层次结构


MySQL 采用了 4 级结构:


  1. 实例
  2. 数据库

Postgres 采用了 5 级结构:


  • 实例(也称为集群)
  • 数据库
  • 模式 Schema

ACID 事务


两个数据库都支持 ACID 事务,Postgres 提供更强大的事务支持。




安全性


Postgres 和 MySQL 都支持 RBAC。


Postgres 支持开箱即用的附加行级安全 (RLS),而 MySQL 需要创建额外的视图来模拟此行为。


查询优化器


Postgres 的查询优化器更优秀,详情参考此吐槽


复制


Postgres 的标准复制使用 WAL 进行物理复制。MySQL 的标准复制使用 binlog 进行逻辑复制。


Postgres 也支持通过其发布/订阅模式进行逻辑复制。


JSON


Postgres 和 MySQL 都支持 JSON。 Postgres 支持的功能更多:


  • 更多操作符来访问 JSON 功能。
  • 允许在 JSON 字段上创建索引。

CTE (Common Table Expression)


Postgres 对 CTE 的支持更全面:


  • 在 CTE 内进行 SELECT, UPDATE, INSERT, DELETE 操作
  • 在 CTE 之后进行 SELECT, UPDATE, INSERT, DELETE 操作

MySQL 支持:


  • 在 CTE 内进行 SELECT 操作
  • 在 CTE 之后进行 SELECT, UPDATE, DELETE 操作

窗口函数 (Window Functions)


窗口帧类型:MySQL 仅支持 Row Frame 类型,允许定义由固定数量行组成的帧;而 Postgres 同时支持 Row Frame 和范围帧类型。


范围单位:MySQL 仅支持 UNBOUNDED PRECEDING 和 CURRENT ROW 这两种范围单位;而 Postgres 支持更多范围单位,包括 UNBOUNDED FOLLOWING 和 BETWEEN 等。


性能:一般来说,Postgres 实现的 Window Functions 比 MySQL 实现更高效且性能更好。


高级函数:Postgres 还支持更多高级 Window Functions,例如 LAG(), LEAD(), FIRST_VALUE(), and LAST_VALUE()。


可扩展性 Extensibility


Postgres 支持多种扩展。最出色的是 PostGIS,它为 Postgres 带来了地理空间能力。此外,还有 Foreign Data Wrapper (FDW),支持查询其他数据系统,pg_stat_statements 用于跟踪规划和执行统计信息,pgvector 用于进行 AI 应用的向量搜索。


MySQL 具有可插拔的存储引擎架构,并诞生了 InnoDB。但如今,在 MySQL 中,InnoDB 已成为主导存储引擎,因此可插拔架构只作为 API 边界使用,而不是用于扩展目的。


在认证方面,Postgres 和 MySQL 都支持可插拔认证模块 (PAM)。


易用性 Usability


Postgres 更加严格,而 MySQL 更加宽容:


  • MySQL 允许在使用 GROUP BY 子句的 SELECT 语句中包含非聚合列;而 Postgres 则不允许。
  • MySQL 默认情况下是大小写不敏感的;而 Postgres 默认情况下是大小写敏感的。
  • MySQL 允许 JOIN 来自不同数据库的表;而 Postgres 只能连接单个数据库内部的表,除非使用 FDW 扩展。

连接模型 Connection Model


Postgres 采用在每个连接上生成一个新进程的方式工作。而 MySQL 则在每个连接上生成一个新线程。因此,Postgres 提供了更好的隔离性,例如,一个无效的内存访问错误只会导致单个进程崩溃,而不是整个数据库服务器。另一方面,进程模型消耗更多资源。因此,在部署 Postgres 时建议通过连接池(如 PgBouncer 或 pgcat)代理连接。


生态 Ecosystem


常见的 SQL 工具都能很好地支持 Postgres 和 MySQL。由于 Postgres 的可扩展架构,并且仍被社区拥有,近年来 Postgres 生态系统更加繁荣。对于提供托管数据库服务的应用平台,每个都选择了 Postgres。从早期的 Heroku 到更新的 Supabase, render 和 Fly.io。


可运维性 Operability


由于底层存储引擎设计问题,在高负载下,Postgres 存在臭名昭著的 XID wraparound 问题。


对于 MySQL,在 Google Cloud 运营大规模 MySQL 集群时,我们遇到过一些复制错误。


这些问题只会在极端负载下发生。对于正常工作负载而言,无论是 Postgres 还是 MySQL 都是成熟且可靠的。数据库托管平台也提供集成备份/恢复和监控功能。


Postgres 还是 MySQL


2023 年了,在 Postgres 和 MySQL 之间做选择仍然很困难,并且经常引起激烈讨论




总的来说,Postgres 有更多功能、更繁荣的社区和生态;而 MySQL 则更易学习并且拥有庞大的用户群体。
我们观察到与 Stack Overflow 结果相同的行业趋势,即 Postgres 在开发者中变得越来越受欢迎。但根据我们的实际体验,精密的 Postgres 牺牲了一些便利性。如果你对 Postgres 不太熟悉,最好从云服务提供商那里启动一个实例,并运行几个查询来上手。有时候,这些额外好处可能并不值得,选择 MySQL 会更容易一些。


同时,在一个组织内部共存 Postgres 和 MySQL 也是很常见的情况。如果需要同时管理 Postgres 和 MySQL 的开发生命周期,可以来了解一下 Bytebase。






💡 你可以访问官网,免费注册云账号,立即体验 Bytebase。


作者:Bytebase
链接:https://juejin.cn/post/7254944931386228796
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

听说你会架构设计?来,解释一下为什么错不在李佳琦

1. 引言 大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。 1.1 带货风波 近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。 图来源:微博热点,侵删 虽然小❤...
继续阅读 »

1. 引言


大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。


1.1 带货风波


近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。



图来源:微博热点,侵删


虽然小❤平时很少看直播,尤其是带货直播。


但奈何不住吃瓜的好奇心重啊!于是就趁着休息的功夫了解了一下,原来这场风波事件起源于前几天的一场直播。


当时,李佳琦在直播间介绍合作产品 “花西子” 眉笔的价格为 79 元时,有网友在评论区吐槽越来越贵了。他直言:哪里贵了?这么多年都是这个价格,不要睁着眼睛乱说,国货品牌很难的,哪里贵了?



图来源:网络,侵删


之后,李佳琦接着表示:有的时候找找自己原因,这么多年了工资涨没涨,有没有认真工作?



图来源:互联网,侵删


小❤觉得,这件事评论区网友说的没错,吐槽一下商品的价格有什么问题呢?我自己平时买菜还挑挑拣拣的,能省一毛是一毛。


毕竟,这个商品的价格也摆在那是不?



图来源:微博热点,侵删


1.2 身份决定立场,立场决定言论


但是,有一说一,从主播的角度呢,我也能理解。毕竟,不同的消费能力,说着自己立场里认可的大实话,也没啥问题。


那问题出在哪呢?


咳咳,两边都没问题,那肯定是评论系统有问题!


一边是年收入十多亿的带货主播,一边是普普通通的老百姓,你评论区为啥不甄别出用户画像,再隔离一下评论?


俗话说:“屁股决定脑袋”,立场不同,言论自然不一样。所以,这个锅,评论系统背定了!


2. 评论系统的特点


正巧,前几天在看关于评论系统的设计方案,且这类架构设计在互联网大厂的面试里出现的频率还是挺高的。所以我们今天就来探讨一下这个热门话题——《海量评论系统的架构设计》。


2.1 需求分析


首先,让我们来了解一下评论系统的特点和主要功能需求。评论系统是网站和应用中不可或缺的一部分,主要分为两种:



  • 一种是列表平铺式,只能发起评论,不能回复;

  • 一种是盖楼式评论,支持无限盖楼回复,可以回复用户的评论。


为了迎合目前大部分网站和应用 App 的需求,我们设计的评论系统采用盖楼式评论


需要满足以下几个功能需求:



评论系统中的观众和主播相当于用户和管理员的角色,其中观众用户可以:



  • 评论发布和回复:用户可以轻松发布评论,回复他人的评论。

  • 点赞和踩:用户可以给评论点赞或踩,以表达自己的喜好。

  • 评论拉取:评论需要按照时间或热度排序,并且支持分页显示。


主播可以:




  • 管理评论:主播可以根据直播情况以及当前一段时间内的总评论数,来判断是否打开 “喜好开关”。




  • 禁言用户:当用户发布了不当言论,或者恶意引流时,主播可以禁言用户一段时间。




  • 举报/删除:系统需要支持主播举报不当评论,并允许主播删除用户的评论。




2.2 非功能需求


除了功能需求,评论系统还需要满足一系列非功能需求,例如应对高并发场景,在海量数据中如何保证系统的稳定运行是一个巨大的挑战。




  • 海量数据:拿抖音直播举例,10 亿级别的用户量,日活约 2 亿,假设平均每 10 个人/天发一条评论,总评论数约 2 千万/天;




  • 高并发量:每秒十万级的 QPS 访问,每秒万级的评论发布量;




  • 用户分布不均匀:某个直播间的用户或者评论区数量,超出普通用户几个数量级;




  • 时间分布不均匀:某个主播可能突然在某个时间点成为热点用户,其评论数量也可能陡增几个数量级。




3. 系统设计


评论系统也具有一个典型社交类系统的特征,可归结为三点:海量数据,高访问量,非均匀性,接下来我们将对评论系统的关键特点和需求做功能设计。


3.1 功能设计


在直播平台或评论系统里,观众可以接收开通提醒,并且评论被回复之后也可以通过手机 App 收到回复消息,所以需要和系统建立 TCP 长连接。


同样地,主播由于要实时上传视频直播流,所以也需要 TCP 连接。架构图如下:



用户或主播上线时,如果是第一次登录,需要从用户长连接管理系统申请一个 TCP 服务器地址信息,然后进行 TCP 连接



不了解 TCP 连接的同学可以看我之前写的这篇文章:听说你会架构设计?来,弄一个打车系统



当观众或主播(统称用户)第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,用户可以通过用户长连接管理系统重新申请一个 TCP 服务器地址(可用地址存储在 Zookeeper 中),拿到 TCP 地址后再发起请求连接到集群的某一台服务器上。


用户系统


用户系统的用户表记录了主播和观众的个人信息,包括用户名、头像和地理位置等信息。


除此之外,用户还需要记录关注信息,比如某个用户关注了哪些直播间。


用户表(user)设计如下:




  • user_id:用户唯一标识




  • name:用户名




  • portrait:头像压缩存储




  • addr:地理位置




  • role:用户角色,观众或主播




直播系统


每次开播后,直播系统通过拉取直播流,和主播设备建立 TCP 长连接。这时,直播系统会记录直播表(live)信息,包括:




  • live_id:一场直播的唯一标识




  • live_room_id:直播间的唯一标识




  • user_id:主播用户ID




  • title:直播主题




参考微博的关注系统,我们可以引入用户关注表(attention),以便用户可以关注直播间信息,并接收其动态和评论通知:



  • user_id:关注者的用户ID。

  • live_room_id:被关注者的直播间ID。


这个表可以用于构建用户和主播之间的社交网络,并实现评论的动态通知。


用户关系表的设计可以支持关注、取消关注和获取关注列表等功能。


在数据库中,使用索引可以提高关系查询的性能。同时,可以定期清理不活跃的关系,以减少存储和维护成本。


评论系统


参考微博的评论系统,我们可以支持多级嵌套评论,让用户能够回复特定评论。


对于嵌套评论的存储,我们可以使用递归结构或层次结构的数据库设计,也可以使用关系型数据库表结构。评论表(comment)字段如下:



  • comment_id:评论唯一标识符,主键。

  • user_id:评论者的用户ID。

  • content:评论内容,可以是文本或富文本。

  • timestamp:评论时间戳。

  • parent_comment_id:如果是回复评论,记录被回复评论的comment_id。

  • live_id:评论所属的直播ID。

  • level:评论级别,用于标识评论的嵌套层级。


除此之外,我们可以根据业务需求添加一些额外字段:如点赞数、踩数、举报数等,以支持更多功能。


推送系统


为了提供及时的评论通知,我们可以设计消息推送系统,当用户收到关注直播间开播,或者有新评论或回复时,系统可以向其发送通知。


通知系统需要支持消息的推送和处理,当直播间关注人数很多或者用户发出了热点评论时,为了保证系统稳定,可以使用消息队列来处理异步任务


此外,在推送时需要考虑消息的去重、过期处理和用户偏好设置等方面的问题。


3.2 性能和安全


除了最基本的功能设计以外,我们还需要结合评论系统的数据量和并发量,考虑如何解决高并发、高性能以及数据安全的问题。


1)高并发处理


评论系统面临着巨大的并发压力,数以万计的用户可能同时发布和查看评论。为了应对这个挑战,我们可以采取以下策略。


分布式架构



采用分布式集群架构,将流量分散到多个服务器上,降低单点故障风险,提升用户的性能体验。


消息队列


引入消息队列,如 Kafka,来处理异步任务。



当直播间开播时,首先获取到关注该直播间的用户,然后将直播间名称、直播主题等信息,放入消息队列。


消息推送系统实时监听消息队列,当获取到开播提醒的 Topic 时,首先从 Redis 获取和用户连接的 TCP 服务器信息,然后将开播消息推送到用户手机上


同样地,当用户评论被回复时,将评论用户名和评论信息通过消息推送系统,也推送到用户手机上。


使用消息队列一方面可以减轻服务器的流量负担,另一方面可以根据用户离线情况,消息推送系统可以将历史消息传入延时队列,当用户重新上线时去拉取这些历史消息,以此提升用户体验。


数据缓存


引入缓存层,如 Redis,用于缓存最新的评论数据,以此减轻数据库负载并提升响应速度。例如,可以根据 LRU 策略缓存直播间最热的评论、用户地理位置等信息,并定时更新。


2)安全和防护


评论系统需要应对敏感词汇、恶意攻击等安全威胁。我们可以采取以下防护措施:


文字过滤


使用文字过滤技术,过滤垃圾评论和敏感词汇。实现时,可以用 Redis 缓存或者布隆过滤器。对比性能,我们这里采用布隆过滤器来实现。


布隆过滤器(Bloom Filter)是一个巧妙设计的数据结构,它的原理是将一个值多次哈希,映射到不同的 bit 位上并记录下来。


当新的值使用时,通过同样的哈希函数,比对各个 bit 位上是否有值:如果这些 bit 位上都没有值,说明这个数不存在;否则,就大概率是存在的。



以上图为例,具体操作流程为:



  1. 假设敏感词汇有 3 个元素{菜狗,尼玛,撒币},哈希函数的个数也设置为 3。我们首先将位数组初始化,将每个位都置为 0。



  1. 然后将集合里的敏感词语通过 3 个哈希函数进行映射,每次映射都会产生一个哈希值,即位数组里的 1.



  1. 当查询词语是否为敏感文字时,用相同的哈希函数进行映射,如果映射的位置有一个不为 1,说明该文字一定不存在于集合元素中。反之,如果 3 个点都为 1,则判定元素存在于集合中。


当然,这可能会产生误判,布隆过滤器一定可以发现重复的值,但也可能将不重复的值判断为重复值。如上图中的 “天气”,虽然都命中了 1,但是它并没有存在于敏感词集合里。


布隆过滤器在处理大量数据时非常有用,比如网页缓存、拼写检查、黑名单过滤等。虽然它有一定的误判率(约为 0.05%),但是其判重的速度和节省空间的优点足以瑕不掩瑜。


用户限制


除了从评论信息上加以限制,我们也可以从用户侧来限制:



  • 用户认证:要求用户登录后才能发布评论,降低匿名评论的风险。

  • 评论限制:根据用户 ID 和直播 ID 进行限流,比如让用户在一分钟之内最多只能发送 10 条的评论。



不知道如何限流的,可以看小❤之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 李佳琦该如何应对?


4.1 文本分析和情感分析


除了可以用布隆过滤器检测出恶意攻击和敏感内容,我们还可以引入文本分析和情感分析技术,使用自然语言处理(NLP)算法来检测不当评论。


并且,通过分析用户的评论内容,可以进行情感分析,以了解用户的情感倾向。



除了算法模块,我们还需要新增一个评论采集系统,定期(比如每天)从数据库里拉取用户的评论数据,传入对象存储服务。


算法模块监听对象存储服务,每天实时拉取训练数据,并获取可用的情感分析和语义理解模型。


当有新的评论出现时,会先调用算法模型,然后根据情感的分析结果来存储评论信息。我们可以在评论表(comment)里面新增一个表示情感正负倾向的字段 emotion,当主播打开喜好开关后,只拉取 emotion 为 TRUE 的评论信息,将“嫌贵的用户”或者 “评价为负面” 的评论设置为不可见。


这样,每次直播时,主播看到的都是情感正向且说话好听的评论,不仅能提升直播激情,还能增加与 “真爱粉” 的互动效果,可谓一箭三雕 🐶


但是,评论调用算法模型势必会牺牲一定的实时性与互动效果,主播也可以在开启直播时可以自己决定是否要打开评论喜好设置,并告知打开后评论会延时一段时间。


4.2 机器学习和推荐算法


除了从主播的角度,评论系统还可以引入机器学习算法来分析用户行为,根据用户的历史评论和喜好。


从观众来说,这可以提高观众的参与度和留存率,增强用户粘性。


从主播来说,可以筛选出真爱粉,脑残粉,甚至死亡芭比粉 🐶。这样,每次主播在直播时,只筛选一部分用户可以发表评论,其余的统统禁言,或者设置为不看用户评论。



除了直播领域,社交领域也经常使用推荐算法来获取评论内容。比如之前有 B 站 UP 主爆出:小红书在同一个帖子下,对女性用户和男性用户展示的评论区是不一样的,甚至评论区是截然相反的观点。


这个小❤没有试验过,大家不妨去看一下😃


5. 小结


目前,评论系统随着移动互联网的直播和社交平台规模不断扩大,许多网站和应用已经实现了社交媒体集成,允许用户使用他们的社交媒体帐户进行评论,增加了互动性和用户参与度。


一些平台也开始使用机器学习和人工智能技术来提供个性化评论推荐,以改善用户体验。


总的来说,评论系统是在线社交和内容互动的重要组成部分,希望看过这篇文章之后,大家以后知道如何应对类似的公关危机,到时候记得回来给我点赞。


什么?你想现在就分享、点赞,加入在看啊!


那你一定是社交领域的优质用户,如果直播间都是你这样的观众,评论系统设计成什么样已经不重要了!Love And Peace ❤



当然,前提是老板们都得时刻反思找找自己的原因,这么多年了有没有认真工作,有没有给打工人涨涨工资 🐶



作者:xin猿意码
来源:juejin.cn/post/7278592935468924963
收起阅读 »

一个烂分页,踩了三个坑!

你好呀,我是歪歪。 前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。 这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的: select *...
继续阅读 »

你好呀,我是歪歪。


前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。


这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的:



select * from table order by priority limit 1;



priority,就是优先级的意思。


按照优先级 order by 然后 limit 取优先级最高(数字越小,优先级越高)的第一条 ,结合业务背景和数据库里面的数据,我立马就意识到了问题所在。


想起了我当年在写分页逻辑的时候,虽然场景和这个完全不一样,但是踩过到底层原理一模一样的坑,这玩意印象深刻,所以立马就识别出来了。


借着这个问题,也盘点一下我遇到过的三个关于分页查询有意思的坑。


职业生涯的第一个生产 BUG


歪师傅职业生涯的第一个生产 BUG 就是一个小小的分页查询。


当时还在做支付系统,接手的一个需求也很简单就是做一个定时任务,定时把数据库里面状态为初始化的订单查询出来,调用另一个服务提供的接口查询订单的状态并更新。


由于流程上有数据强校验,不用考虑数据不存在的情况。所以该接口可能返回的状态只有三种:成功,失败,处理中。


很简单,很常规的一个需求对吧,我分分钟就能写出伪代码:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);    
    } catch (Exception e){
        //打印异常
    }
}

来,你说上面这个程序有什么问题?



其实在绝大部分情况下都没啥大问题,数据量不多的情况下程序跑起来没有任何毛病。


但是,如果数据量多起来了,一次性把所有初始化状态的订单都拿出来,是不是有点不合理了,万一把内存给你撑爆了怎么办?


所以,在我已知数据量会很大的情况下,我采取了分批次获取数据的模式,假设一次性取 100 条数据出来玩。


那么 SQL 就是这样的:



select * from order where order_status=0 order by create_time limit 100;



所以上面的伪代码会变成这样:


while(true){
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit 100;
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

来,你又来告诉我上面这一段逻辑有什么问题?



作为程序员,我们看到 while(true) 这样的写法立马就要警报拉满,看看有没有死循环的风险。


那你说上面这段代码在什么时候退不出来?


当有任何一条数据的状态没有从初始化变成成功、失败或者处理中的时候,就会导致一直循环。


而虽然发起 RPC 调用的地方,服务提供方能确保返回的状态一定是成功、失败、处理中这三者之中的一个,但是这个有一个前提是接口调用正常的情况下。


如果接口调用一旦异常,那么按照上面的写法,在抛出异常后,状态并未发生变化,还会是停留在“初始化”,从而导致死循环。


当年,测试同学在测试阶段直接就测出了这个问题,然后我对其进行了修改。


我改变了思路,把每次分批次查询 100 条数据,修改为了分页查询,引入了 PageHelper 插件:


//是否是最后一页
while(pageInfo.isLastPage){
    pageNum=pageNum+1;
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit pageNum*100,100;
    PageHelper.startPage(pageNum,100);
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    pageInfo = new PageInfo(initOrderInfoList);
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

跳出循环的条件为判断当前页是否是最后一页。


由于每循环一次,当前页就加一,那么理论上讲一定会是翻到最后一页的,没有任何毛病,对不对?


我们可以分析一下上面的代码逻辑。


假设,我们有 120 条 order_status=0 的数据。


那么第一页,取出了 100 条数据:



SELECT * from order_info WHERE order_status=0 LIMIT 0,100;



这 100 条处理完成之后,第二页还有数据吗?


第二页对应的 sql 为:



SELECT * from order_info WHERE order_status=0 LIMIT 100,100;



但是这个时候,状态为 0 的数据,只有 20 条了,而分页要从第 100 条开始,是不是获取不到数据,导致遗漏数据了?


确实一定会翻到最后一页,解决了死循环的问题,但又有大量的数据遗漏怎么办呢?



当时我苦思冥想,想到一个办法:导致数据遗漏的原因是因为我在翻页的时候,数据状态在变化,导致总体数据在变化。


那么如果我每次都从后往前取数据,每次都固定取最后一页,能取到数据就代表还有数据要处理,循环结束条件修改为“当前页即是第一页,也是最后一页时”就结束,这样不就不会遗漏数据了?


我再给你分析一下。


假设,我们有 120 条 order_status=0 的数据,从后往前取了 100 天出来进行出来,有 90 条处理成功,10 条的状态还是停留在“处理中”。


第二次再取的时候,会把剩下的 20 条和这次“处理中”的 10 条,共计 30 条再次取出来进行处理。


确保没有数据遗漏。


后来测试环节验收通过了,这个方案上线之后,也确实没有遗漏过数据了。


直到后来又一天,提供 queryOrderStatus 接口的服务异常了,我发过去的请求超时了。


导致我取出来的数据,每一条都会抛出异常,都不会更新状态。从而导致我每次从后往前取数据,都取到的是同一批数据。


从程序上的表现上看,日志疯狂的打印,但是其实一直在处理同一批,就是死循环了。


好在我当时还在新手保护期,领导帮我扛下来了。


最后随着业务的发展,这块逻辑也完全发生了变化,逻辑由我们主动去调用 RPC 接口查询状态变成了,下游状态变化后进行 MQ 主动通知,所以我这一坨骚代码也就随之光荣下岗。


我现在想了一下,其实这个场景,用分页的思想去取数据真的不好做。


还不如用最开始的分批次的思想,只不过在会变化的“状态”之外,再加上另外一个不会改变的限定条件,比如常见的创建时间:



select * from order where order_status=0 and create_time>xxx order by create_time limit 100;



最好不要基于状态去做分页,如果一定要基于状态去做分页,那么要确保状态在分页逻辑里面会扭转下去。


这就是我职业生涯的第一个生产 BUG,一个低级的分页逻辑错误。


还是分页,又踩到坑


这也是在工作的前两年遇到的一个关于分页的坑。


最开始在学校的时候,大家肯定都手撸过分页逻辑,自己去算总页数,当前页,页面大小啥的。


当时功力尚浅,觉得这部分逻辑写起来是真复杂,但是扣扣脑袋也还是可以写出来。


后来参加工作了之后,在项目里面看到了 PageHelper 这个玩意,了解之后发了“斯国一”的惊叹:有了这玩意,谁还手写分页啊。



但是我在使用 PageHelper 的时候,也踩到过一个经典的“坑”。


最开始的时候,代码是这样的:


PageHelper.startPage(pageNum,100);
List<OrderInfo> list = orderInfoMapper.select(param1);

后来为了避免不带 where 条件的全表查询,我把代码修改成了这样:


PageHelper.startPage(pageNum,100);
if(param != null){
    List<OrderInfo> list = orderInfoMapper.select(param);
}

然后,随着程序的迭代,就出 BUG 了。因为有的业务场景下,param 参数一路传递进来之后就变成了 null。


但是这个时候 PageHelper 已经在当前线程的 ThreadLocal 里面设置了分页参数了,但是没有被消费,这个参数就会一直保留在这个线程上,也就是放在线程的 ThreadLocal 里面。


当这个线程继续往后跑,或者被复用的时候,遇到一条 SQL 语句时,就可能导致不该分页的方法去消费这个分页参数,产生了莫名其妙的分页。


所以,上面这个代码,应该写成下面这个样子:


if(param != null){
    PageHelper.startPage(pageNum,100);
    List<OrderInfo> list = orderInfoMapper.select(param);
}

也是这次踩坑之后,我翻阅了 PageHelper 的源码,了解了底层原理,并总结了一句话:需要保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,否则会污染线程。


在正确使用 PageHelper 的情况下,其插件内部,会在 finally 代码段中自动清除了在 ThreadLocal 中存储的对象。


这样就不会留坑。


这次翻页源码的过程影响也是比较深刻的,虽然那个时候经验不多,但是得益于 MyBatis 的源码和 PageHelper 的源码写的都非常的符合正常人的思维,阅读起来门槛不高,再加上我有具体的疑问,所以那是一次古早时期,尚在新手村时,为数不多的,阅读源码之后,感觉收获满满的经历。


分页丢数据


关于这个 BUG 可以说是印象深刻了。


当年遇到这个坑的时候排查了很长时间没啥头绪,最后还是组里的大佬指了条路。


业务需求很简单,就是在管理页面上可以查询订单列表,查询结果按照订单的创建时间倒序排序。


对应的分页 SQL 很简单,很常规,没有任何问题:



select * from table order by create_time desc limit 0,10;



但是当年在页面上的表现大概是这样的:



订单编号为 5 的这条数据,会同时出现在了第一页和第二页。


甚至有的数据在第二页出现了之后,在第五页又出现一次。


后来定位到产生这个问题的原因是因为有一批数量不小的订单数据是通过线下执行 SQL 的方式导入的。


而导入的这一批数据,写 SQL 的同学为了方便,就把 create_time 都设置为了同一个值,比如都设置为了 2023-09-10 12:34:56 这个时间。


由于 create_time 又是我作为 order by 的字段,当这个字段的值大量都是同一个值的时候,就会导致上面的一条数据在不同的页面上多次出现的情况。


针对这个现象,当时组里的大佬分析明白之后,扔给我一个链接:



dev.mysql.com/doc/refman/…



这是 MySQL 官方文档,这一章节叫做“对 Limit 查询的优化”。


开篇的时候人家就是这样说的:



如果将 LIMIT row_count 和 ORDER BY 组合在一起,那么 MySQL 在找到排序结果的第一行 count 行时就停止排序,而不是对整个结果进行排序。


然后给了这一段补充说明:



如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


换句话说,相对于未排序列,这些记录的排序顺序是 nondeterministic 的:



然后官方给了一个示例。


首先,不带 limit 的时候查询结果是这样的:



基于这个结果,如果我要取前五条数据,对应的 id 应该是 1,5,3,4,6。


但是当我们带着 limit 的时候查询结果可能是这样的:



对应的 id 实际是 1,5,4,3,6。


这就是前面说的:如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


从程序上的表现上来看,结果就是 nondeterministic。


所以看到这里,我们大概可以知道我前面遇到的分页问题的原因是因为那一批手动插入的数据对应的 create_time 字段都是一样的,而 MySQL 这边又对 Limit 参数做了优化,运行结果出现了不确定性,从而页面上出现了重复的数据。


而回到文章最开始的这个 SQL,也就是我一眼看出问题的这个 SQL:



select * from table order by priority limit 1;



因为在我们的界面上,只是约定了数字越小优先级越高,数字必须大于 0。


所以当大家在输入优先级的时候,大部分情况下都默认自己编辑的数据对应的优先级最高,也就是设置为 1,从而导致数据库里面有大量的优先级为 1 的数据。


而程序每次处理,又只会按照优先级排序只会,取一条数据出来进行处理。


经过前面的分析我们可以知道,这样取出来的数据,不一定每次都一样。


所以由于有这段代码的存在,导致业务上的表现就很奇怪,明明是一模一样的请求参数,但是最终返回的结果可能不相同。


好,现在,我问你,你说在前面,我给出的这样的分页查询的 SQL 语句有没有毛病?



select * from table order by create_time desc limit 0,10;



没有任何毛病嘛,执行结果也没有任何毛病?


有没有给你按照 create_time 排序?


摸着良心说,是有的。


有没有给你取出排序后的 10 条数据?


也是有的。


所以,针对这种现象,官方的态度是:我没错!在我的概念里面,没有“分页”这样的玩意,你通过组合我提供的功能,搞出了“分页”这种业务场景,现在业务场景出问题了,你反过来说我底层有问题?


这不是欺负老实人吗?我没错!



所以,官方把这两种案例都拿出来,并且强调:



在每种情况下,查询结果都是按 ORDER BY 的列进行排序的,这样的结果是符合 SQL 标准的。




虽然我没错,但是我还是可以给你指个路。


如果你非常在意执行结果的顺序,那么在 ORDER BY 子句中包含一个额外的列,以确保顺序具有确定性。


例如,如果 id 值是唯一的,你可以通过这样的排序使给定类别值的行按 id 顺序出现。


你这样去写,排序的时候加个 id 字段,就稳了:



好了,如果觉得本文对你有帮助的话,求个免费的点赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7277187894870671360
收起阅读 »

你知道抖音的IP归属地是怎么实现的吗

1.背景 最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎...
继续阅读 »

1.背景


最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎么解析IP获取归属地问题。


接下来,就着重讲解一下Java后端怎么实现IP归属地的功能,其实只需要以下两大步骤:


2.获取客户端ip接口


做过web开发都知道,无论移动端还是pc端的请求接口都会被封装成为一个HttpServletRequest对象,该对象包含了客户端请求信息包括请求的地址,请求的参数,提交的数据等等。


如果服务器直接把IP暴漏出去,那么request.getRemoteAddr()就能拿到客户端ip。


但目前流行的架构中,基本上服务器都不会直接把自己的ip暴漏出去,一般前面还有一层或多层反向代理,常见的nginx居多。 加了代理后,相当于服务器和客户端中间还有一层,这时·request.getRemoteAddr()拿到的就是代理服务器的ip了,并不是客户端的ip。所以这种情况下,一般会在转发头上加X-Forwarded-For等信息,用来跟踪原始客户端的ip。


X-Forwarded-For: 这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项。 格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。 上面的代码注释也说的很清楚,直接截取拿到第一个ip。 Proxy-Client-IP/WL- Proxy-Client-IP: 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。这种情况也是直接能拿到。 HTTP_CLIENT_IP: 有些代理服务器也会加上此请求头。 X-Real-IP: nginx一般用这个。


但是在日常开发中,并没有规范规定用以上哪一个头信息去跟踪客户端,所以都有可能,只能一一尝试,直到获取到为止。代码如下:


@Slf4j
public class IpUtils {

   private static final String UNKNOWN_VALUE = "unknown";
   private static final String LOCALHOST_V4 = "127.0.0.1";
   private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";

   private static final String X_FORWARDED_FOR = "X-Forwarded-For";
   private static final String X_REAL_IP = "X-Real-IP";
   private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
   private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
   private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";

   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;
 
  /**
    * 获取客户端ip地址
    * @param request
    * @return
    */
   public static String getRemoteHost(HttpServletRequest request) {
       String ip = request.getHeader(X_FORWARDED_FOR);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           // 多次反向代理后会有多个ip值,第一个ip才是真实ip
           int index = ip.indexOf(",");
           if (index != -1) {
               return ip.substring(0, index);
          } else {
               return ip;
          }
      }
       ip = request.getHeader(X_REAL_IP);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           return ip;
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(WL_PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(HTTP_CLIENT_IP);
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }
       return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip;
  }

}


项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记


交流探讨群:Shepherd_126



3.获取ip归属地


通过上面我们就能获取到客户端用户的ip地址,接下来就可以通过ip解析获取归属地了。


如果我们在网上搜索资料教程,大部分都是说基于各大平台(eg:淘宝,新浪)提供的ip库进行查询,不过不难发现这些平台已经不怎么维护这个功能,现在处于“半死不活”的状态,根本不靠谱,当然有些平台提供可靠的获取ip属地接口,但是收费、收费、收费


本着作为一个程序员的严谨:“能白嫖的就白嫖,避免出现要买的是你,不会用也是你的尴尬遭遇”。扯远了言归正传,为了寻求可靠有效的解决方案,只能去看看github有没有什么项目能满足需求,果然功夫不负有心人,发现一个宝藏级项目:ip2region,一个准确率 99.9% 的离线 IP 地址定位库,0.0x 毫秒级查询,ip2region.db 数据库只有数 MB的项目,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,这里只能说:开源真香,开源万岁。


3.1 Ip2region 特性


标准化的数据格式


每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,其余选项全部是0。


数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


IP 数据管理框架


v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


99.9% 准确率


数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。


ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次):



备注:如果上述开放 API 或者数据都不给开放数据时 ip2region 将停止数据的更新服务。


3.2 整合Ip2region客户端进行查询


提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5 和 php7)、golang、rust、lua、lua_c,nginx。这里讲一下java的客户端。


首先我们需要引入依赖:


<dependency>
 <groupId>org.lionsoul</groupId>
 <artifactId>ip2region</artifactId>
 <version>2.6.5</version>
</dependency>

接下来我们需要先去下载数据文件ip2region.xdb到本地,然后基于数据文件进行查询,下面查询方法文件路径改为你本地路径即可,ip2region提供三种查询方式:


完全基于文件的查询


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       // 1、创建 searcher 对象
       String dbPath = "ip2region.xdb file path";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (IOException e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();
       
       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }
}

缓存 VectorIndex 索引


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
       byte[] vIndex;
       try {
           vIndex = Searcher.loadVectorIndexFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
      } catch (Exception e) {
           System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源
       searcher.close();

       // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
  }
}

缓存整个 xdb 数据


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 加载整个 xdb 到内存。
       byte[] cBuff;
       try {
           cBuff = Searcher.loadContentFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithBuffer(cBuff);
      } catch (Exception e) {
           System.out.printf("failed to create content cached searcher: %s\n", e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
       // searcher.close();

       // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
  }
}

3.3 springboot整合示例


首先我们也需要像上面一样引入maven依赖。然后就可以基于上面的查询方式进行封装成工具类了,我这里选择了上面的第三种方式:缓存整个 xdb 数据


@Slf4j
public class IpUtils {
   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;

   static {
       try {
           // 从 dbPath 加载整个 xdb 到内存。
           contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }
 
     /**
    * 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信
    * @param ip
    * @return
    */
   public static IpRegion getIpRegion(String ip) {
       Searcher searcher = null;
       IpRegion ipRegion = new IpRegion();
       try {
           searcher = Searcher.newWithBuffer(contentBuff);
           String region = searcher.search(ip);
           String[] info = StringUtils.split(region, "|");
           ipRegion.setCountry(info[0]);
           ipRegion.setArea(info[1]);
           ipRegion.setProvince(info[2]);
           ipRegion.setCity(info[3]);
           ipRegion.setIsp(info[4]);
      } catch (Exception e) {
           log.error("get ip region error: ", e);
      } finally {
           if (searcher != null) {
               try {
                   searcher.close();
              } catch (IOException e) {
                   log.error("close searcher error:", e);
              }
          }
      }
       return ipRegion;
  }

}

作者:shepherd111
来源:juejin.cn/post/7280118836685668367
收起阅读 »

问个事,我就用Tomcat,不用Nginx,行不行!

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx! 不用Nginx,只用Tomcat的Http请求流程 浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请...
继续阅读 »

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx!


不用Nginx,只用Tomcat的Http请求流程


浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请求到对应的IP地址。以阿里云域名管理服务为例,一个域名可以最多绑定三个IP地址,这三个IP地址需要是公网IP地址,所以首先需要在三个公网Ip服务器上部署Tomcat实例。


此时我将面临的麻烦如下



  1. 由于DNS域名管理绑定的IP地址有限,最多三个,你如果想要扩容4台Tomcat,是不支持的。无法满足扩容的诉求

  2. 如果你有10个服务,对应10套Tomcat集群,就需要10 * 3台公网Ip服务器。成本还是蛮高的。

  3. 10个服务需要对应10个域名,分别映射到对应的Tomcat集群

  4. 10个域名我花不起这个钱啊!(其实可以用二级域名配置DNS映射)

  5. 公网服务器作为接入层需要有防火墙等安全管控措施,30台公网服务器,网络安全运维,我搞不定。

  6. 公网IP地址需要额外从移动联通运营商或云厂商购买,30个公网IP价格并不便宜。

  7. 前后端分离的情况,Tomcat无法作为静态文件服务器,只能用Nginx或Apache


以上几个问题属于成本、安全、服务扩容等方面。


如果Tomcat服务发布怎么办


Tomcat在服务发布期间是不可用的,在发布期间Http请求打到发布的服务器,就会失败。由于DNS 最多配置3台服务器,也就是发布期间是 1/3 的失败率。 我会被老板枪毙,用加特林


DNS不能自动摘掉故障的IP地址吗?


不能,DNS只是负责解析域名对应的IP地址,他并不知道对应的服务器状态,更不会知道服务器上Tomcat的状态如何。DNS只是解析IP,并没有转发Http请求,所以压根不知道哪台服务器故障率高。更无法自动摘掉IP地址。


我能手动下掉故障的IP地址吗?


这个我能,但是还是会有大量请求失败。以阿里云为例,配置域名映射时,我可以下掉对应的IP地址,但需要指定域名映射的缓存时间,默认10分钟。换句话说,就算你在上线前,摘掉了对应的IP,依然要等10分钟,所有的客户端才会拿到最新的DNS解析地址。


那么把TTL缓存时间改小,可以吗? 可以的,但是改小了,就意味更多的请求被迫从DNS服务器拿最新的映射,整体请求耗时增加,用户体验下降!被老板发现,会骂我。


节点突然挂掉怎么办?


虽然可以在DNS管理后台手动下掉IP地址,但是节点突然宕机、Tomcat Crash等因素导致的突然故障,我是来不及下掉对应IP地址的,我只能打电话告诉老板,“线上服务崩了,你等我10分钟改点东西”。


如果这时候有个软件能 对Tomcat集群健康检查和故障重试,那就太好了。


恰好,这是 Nginx 的长处!


Nginx可以健康检查和故障重试


而Tomcat没有。


例如有两台Tomcat节点,在Nginx配置故障重试策略


upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
}

当A节点出现 connect refused时(端口关闭或服务器挂了),说明服务不可用,可能是服务发布,也可能是服务器挂了。此时nginx会把失败的请求自动转发到B节点。 假设第二个请求 请求到A还是失败,正好累计2个失败了,那么Nginx会自动把A节点剔除存活列表 60 秒,然后继续把请求2 转发到B节点进行处理。60秒后,再次尝试转发请求到A节点…… 循环往复,直至A节点活过来……


而这一过程客户端是感知不到失败的。因为两次请求都二次转发到B节点成功处理了。客户端并不会感知到A节点的处理失败,这就是Nginx 反向代理的好处。即客户端不用直连服务端,加了个中间商,服务端的个别节点宕机或发布,对客户端都毫无影响。


而Tomcat只是Java Web容器,并不能做这些事情。


10个服务,10个Tomcat集群,就要10个域名,30个公网IP吗?


以阿里云为例,域名管理后台是可以配置二级域名映射,所以一个公网域名拆分为10个二级域名就可以了。


所以只用Tomcat,不用Nginx。需要1个公网域名,10个二级域名,30台服务器、30个公网IP。


当我和老板提出这些的时候,他跟我说:“你XX疯了,要不滚蛋、要不想想别的办法。老子没钱,你看我脑袋值几个钱,拿去换公网IP吧”。


image.png


心里苦啊,要是能有一个软件,能帮我把一个域名分别映射到30个内网IP就好了。


恰好 Nginx可以!


Nginx 虚拟主机和反向代理


例如把多个二级域名映射到不同的文件目录,例如



  1. bbs.abc.com,映射到 html/bbs

  2. blog.abc.com 映射到 html/blog


http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name http://www.abc.com;
location / {
root html/www;
index index.html index.htm;
}
}

server {
listen 80;
server_name bbs.abc.com;
location / {
root html/bbs;
index index.html index.htm;
}
}

server {
listen 80;
server_name blog.abc.com;
location / {
root html/blog;
index index.html index.htm;
}
}
}

例如把不同的二级域名或者URL路径 映射到不同的 Tomcat集群



  1. 分别定义 serverGroup1、serverGroup2 两个Tomcat集群

  2. 分别把路径group1、group1 反向代理到serverGroup1、serverGroup2


upstream serverGroup1 {                    # 定义负载均衡设备的ip和状态
server 192.168.225.100:8080 ; # 默认权重值为一
server 192.168.225.101:8082 weight=2; # 值越高,负载的权重越高
server 192.168.225.102:8083 ;
server 192.168.225.103:8084 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

upstream serverGroup2 { # 定义负载均衡设备的ip和状态
server 192.168.225.110:8080 ; # 默认权重值为一
server 192.168.225.111:8080 weight=2; # 值越高,负载的权重越高
server 192.168.225.112:8080 ;
server 192.168.225.113:8080 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

server { # 设定虚拟主机配置
listen 80; # 监听的端口
server_name picture.itdragon.com; # 监听的地址,多个域名用空格隔开
location /group1 { # 默认请求 ,后面 "/group1" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup1; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}
location /group2 { # 默认请求 ,后面 "/group2" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup2; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}

error_page 500 502 503 504 /50x.html;# 定义错误提示页面
location = /50x.html { # 配置错误提示页面
root html;
}
}

经过以上的教训,我再也不会犯这么愚蠢的错误了,我需要Tomcat,也需要Nginx。


当然如果钱足够多、资源无限丰富,公网IP、公网服务器、域名无限…… 服务发布,网站崩溃,无动于衷,可以不用Nginx。


作者:他是程序员
来源:juejin.cn/post/7280088532377534505
收起阅读 »

什么是 HTTP 长轮询?

什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。




上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:


  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。
  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。

HTTP 长轮询解决了使用 HTTP 进行轮询的缺点


  1. 请求从浏览器发送到服务器,就像以前一样
  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送
  3. 客户端等待服务器的响应。
  4. 当数据可用时,服务器将其发送给客户端
  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。


  • 浏览器将始终在可用时接收最新更新
  • 服务器不会被永远无法满足的请求所搞垮。

长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。


  • 随着使用量的增长,您将如何编排实时后端?
  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?
  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?
  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?

在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。




然后出现几个明显的问题:


  • 服务器应该将数据缓存或排队多长时间?
  • 应该如何处理失败的客户端连接?
  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?
  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?

所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。


作者:demo007x
链接:https://juejin.cn/post/7240111396869161020
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

趣解设计模式之《小王看病记》

〇、小故事 小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。 小王在项目期间也经常因为饮食不规范而导致胃疼,最近也...
继续阅读 »

〇、小故事


小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。


小王在项目期间也经常因为饮食不规范而导致胃疼,最近也越来越严重了。所以他就想趁着这个假期时间去医院检查一下身体


他来到医院的挂号处,首先缴费挂号,挂了一个检查胃部的诊室。



小王按照挂号信息,来到了诊室,医生简单的询问了一下他的病情,然后给他开了几个需要检查的单子



小王带着医生开具的检查单,就在医院的收费处排队等待着缴费



缴费完毕后,小王就按照医生开的检查项目进行了身体检查……



那么从上面小王的一系列看病流程我们可以发现,这是一系列的处理过程,跟链条一样,即:



挂号——>开检查单——>缴费——>检查——>……



那么对于类似这种的业务逻辑,我们就可以使用一种设计模式来处理,即今天要介绍的——责任链模式


一、模式定义


责任链模式Chain of Responsibility Pattern



使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。



二、模式类图


下面我们再举一个例子,一家公司收到了好多的电子邮件,其中大致分为四类:



CEO处理】公司粉丝发来的邮件。

法律部门处理诽谤公司产品的邮件。

业务部门处理】要求业务合作的邮件。

直接丢弃】其他垃圾邮件。



这里需要CEO先查阅邮件处理,然后再由法务部处理,随后是业务部处理,最后是垃圾邮件执行废弃。根据以上的描述,我们首先需要邮件实体类Email,和用于区分不同处理方式的邮件类型EmailType。对于所有处理者,我们首先创建一个抽象的处理器类AbstractProcessor,再创建四个处理器的实现类,分别是CEO处理器CeoProcessor法务部门处理器LawProcessor业务部门处理器BusinessProcessor垃圾邮件处理器GarbageProcessor。具体类关系如下图所示:



三、模式实现


创建邮件实体类Email.java


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Email {
// 邮件类型
private int type;

// 邮件内容
private String content;
}

创建邮件类型枚举类EmailType.java


public enum EmailType {
FANS_EMAIL(1, "粉丝邮件"),
SLANDER_EMAIL(2, "诽谤邮件"),
COOPERATE_EMAIL(3, "业务合作邮件"),
GARBAGE_EMAIL(99, "垃圾邮件");

public int type;

public String remark;

EmailType(int type, String remark) {
this.type = type;
this.remark = remark;
}
}

创建抽象处理类AbstractProcessor.java


public abstract class AbstractProcessor {

// 责任链中下一个处理节点
private AbstractProcessor nextProcessor;

// 返回的处理结果
private String result;

public final String handleMessage(List emails) {
List filterEmails =
emails.stream().filter(email -> email.getType() == this.emailType()).collect(Collectors.toList());
result = this.execute(filterEmails);
if (this.nextProcessor == null) {
return result;
}
return this.nextProcessor.handleMessage(emails);
}

// 设置责任链的下一个处理器
public void setNextProcessor(AbstractProcessor processor) {
this.nextProcessor = processor;
}

// 获得当前Processor可以处理的邮件类型
protected abstract int emailType();

// 具体处理方法
protected abstract String execute(List emails);
}

创建CEO处理类CeoProcessor.java


public class CeoProcessor extends AbstractProcessor {
@Override
protected int emailType() {
return EmailType.FANS_EMAIL.type; // 处理粉丝来的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------CEO开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建法律部门处理类LawProcessor.java


public class LawProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.SLANDER_EMAIL.type; // 处理诽谤类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------法律部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建业务部门处理类BusinessProcessor.java


public class BusinessProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.COOPERATE_EMAIL.type; // 处理合作类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------业务部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建垃圾邮件处理类GarbageProcessor.java


public class GarbageProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.GARBAGE_EMAIL.type; // 处理垃圾类型邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------垃圾开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建责任链模式测试类ChainTest.java


public class ChainTest {
// 初始化待处理邮件
private static List emails = Lists.newArrayList(
new Email(EmailType.FANS_EMAIL.type, "我是粉丝A"),
new Email(EmailType.COOPERATE_EMAIL.type, "我要找你们合作"),
new Email(EmailType.GARBAGE_EMAIL.type, "我是垃圾邮件"),
new Email(EmailType.FANS_EMAIL.type, "我是粉丝B"));

public static void main(String[] args) {
// 初始化处理类
AbstractProcessor ceoProcessor = new CeoProcessor();
AbstractProcessor lawProcessor = new LawProcessor();
AbstractProcessor businessProcessor = new BusinessProcessor();
AbstractProcessor garbageProcessor = new GarbageProcessor();

// 设置责任链条
ceoProcessor.setNextProcessor(lawProcessor);
lawProcessor.setNextProcessor(businessProcessor);
businessProcessor.setNextProcessor(garbageProcessor);

// 开始处理邮件
ceoProcessor.handleMessage(emails);
}
}

执行后的结果


-------CEO开始处理邮件-------
我是粉丝A
我是粉丝B
-------业务部门开始处理邮件-------
我要找你们合作
-------垃圾开始处理邮件-------
我是垃圾邮件

Process finished with exit code 0

作者:爪哇缪斯
来源:juejin.cn/post/7277801611996676157
收起阅读 »

微博图床挂了!

一直担心的事情还是发生了。 作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然...
继续阅读 »

一直担心的事情还是发生了。


作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然今年早些的时候,部分如「ws1、ws2……」的域名就已经无法使用了,但通过某些手段还是可以让其存活的,而最近,所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。




💡Tips:图片中出现的Tengine是淘宝在Nginx的基础上修改后开源的一款Web服务器,基本上,Tengine可以被看作一个更好的Nginx,或者是Nginx的超集,详情可参考👉淘宝Web服务器Tengine正式开源 - The Tengine Web Server



刚得知这个消息的时候,我的第一想法其实是非常生气的,毕竟自己这几年上千张图片都是用的微博图床,如今还没备份就被403了,可仔细一想,说到底还是把东西交在别人手里的下场,微博又不是慈善企业,也要控制成本,一直睁一只眼闭一只眼让大家免费用就算了,出了问题还是不太好怪到微博上来的。


那么有什么比较好的办法解决这个问题呢?


查遍了网上一堆复制/粘贴出来的文章,不是开启反向代理就是更改请求头,真正愿意从根本上解决问题的没几个。


如果不想将自己沉淀的博客、文章托管在印象笔记、notion、语雀这些在线平台的话,想要彻底解决这个问题最好的方式是:自建图床!


为了更好的解决问题,我们先弄明白,403是什么,以及我们存在微博上的图片究竟是如何被403的。


403


百度百科,对于403错误的解释很简单



403错误是一种在网站访问过程中,常见的错误提示,表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录的权限设置导致的WEB访问错误。



所以说到底是因为访问者无权访问服务器端所提供的资源。而微博图床出现403的原因主要在于微博开启了防盗链。


防盗链的原理很简单,站点在得知有请求时,会先判断请求头中的信息,如果请求头中有Referer信息,然后根据自己的规则来判断Referer头信息是否符合要求,Referer 信息是请求该图片的来源地址。


如果盗用网站是 https 的 协议,而图片链接是 http 的话,则从 https 向 http 发起的请求会因为安全性的规定,而不带 referer,从而实现防盗链的绕过。官方输出图片的时候,判断了来源(Referer),就是从哪个网站访问这个图片,如果是你的网站去加载这个图片,那么 Referer 就是你的网站地址;你的网址肯定没在官方的白名单内,(当然作为可操作性极强的浏览器来说 referer 是完全可以伪造一个官方的 URL 这样也也就也可以饶过限制🚫)所以就看不到图片了。



解决问题


解释完原理之后我们发现,其实只要想办法在自己的个人站点中设置好referer就可以解决这个问题,但说到底也只是治标不治本,真正解决这个问题就是想办法将图片迁移到自己的个人图床上。


现在的图床工具很多,iPic、uPic、PicGo等一堆工具既免费又开源,问题在于选择什么云存储服务作为自己的图床以及如何替换自己这上千张图片。



  1. 选择什么云存储服务

  2. 如何替换上千张图片


什么是OSS以及如何选择


「OSS」的英文全称是Object Storage Service,翻译成中文就是「对象存储服务」,官方一点解释就是对象存储是一种使用HTTP API存储和检索非结构化数据和元数据对象的工具。


白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传等一列服务,这样的服务以及技术可以统称为OSS,业内提供OSS服务的厂商很多,知名常用且成规模的有阿里云、腾讯云、百度云、七牛云、又拍云等。


对于我们这些个人用户来说,这些云厂商提供的服务都是足够使用的,我们所要关心的便是成本💰。


笔者使用的是七牛云,它提供了10G的免费存储,基本已经够用了。


有人会考虑将GitHub/Gitee作为图床,并且这样的文章在中文互联网里广泛流传,因为很多人的个人站点都是托管在GitHub Pages上的,但是个人建议是不要这么做。


首先GitHub在国内的访问就很受限,很多场景都需要科学上网才能获得完整的浏览体验。再加上GitHub官方也不推荐将Git仓库存储大文件,GitHub建议仓库保持较小,理想情况下小于 1 GB,强烈建议小于 5 GB。


如何替换上千张图片


替换文章中的图片链接和“把大象放进冰箱里”步骤是差不多的



  1. 下载所有的微博图床的图片

  2. 上传所有的图片到自己的图床(xx云)

  3. 对文本文件执行replaceAll操作


考虑到我们需要迁移的文件数量较多,手动操作肯定是不太可行的,因此我们可以采用代码的方式写一个脚本完成上述操作。考虑到自己已经是一个成熟的Java工程师了,这个功能就干脆用Java写了。


为了减少代码量,精简代码结构,我这里引入了几个第三方库,当然不引入也行,如果不引入有一些繁琐而又简单的业务逻辑需要自己实现,有点浪费时间了。


整个脚本逻辑非常简单,流程如下:



获取博客文件夹下的Markdown文件


这里我们直接使用hutool这个三方库,它内置了很多非常实用的工具类,获取所有markdown文件也变得非常容易


/**
* 筛选出所有的markdown文件
*/

public static List<File> listAllMDFile() {
List<File> files = FileUtil.loopFiles(VAULT_PATH);
return files.stream()
.filter(Objects::nonNull)
.filter(File::isFile)
.filter(file -> StringUtils.endsWith(file.getName(), ".md"))
.collect(Collectors.toList());
}

获取文件中的所有包含微博图床的域名


通过Hutools内置的FileReader我们可以直接读取markdown文件的内容,因此我们只需要解析出文章里包含微博图床的链接即可。我们可以借助正则表达式快速获取一段文本内容里的所有url,然后做一下filter即可。


/**
* 获取一段文本内容里的所有url
*
* @param content 文本内容
* @return 所有的url
*/

public static List<String> getAllUrlsFromContent(String content) {
List<String> urls = new ArrayList<>();
Pattern pattern = Pattern.compile(
"\\b(((ht|f)tp(s?)\\:\\/\\/|~\\/|\\/)|www.)" + "(\\w+:\\w+@)?(([-\\w]+\\.)+(com|org|net|gov"
+ "|mil|biz|info|mobi|name|aero|jobs|museum" + "|travel|[a-z]{2}))(:[\\d]{1,5})?"
+ "(((\\/([-\\w~!$+|.,=]|%[a-f\\d]{2})+)+|\\/)+|\\?|#)?" + "((\\?([-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)" + "(&(?:[-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)*)*" + "(#([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)?\\b");
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
urls.add(matcher.group());
}
return urls;
}

下载图片


用Java下载文件的代码在互联网上属实是重复率最高的一批检索内容了,这里就直接贴出代码了。


public static void download(String urlString, String fileName) throws IOException {
File file = new File(fileName);
if (file.exists()) {
return;
}
URL url = null;
OutputStream os = null;
InputStream is = null;
try {
url = new URL(urlString);
URLConnection con = url.openConnection();
// 输入流
is = con.getInputStream();
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流
os = Files.newOutputStream(Paths.get(fileName));
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
} finally {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
}
}

上传图片


下载完图片后我们便要着手将下载下来的图片上传至我们自己的云存储服务了,这里直接给出七牛云上传图片的文档链接了,文档里写的非常详细,我就不赘述了👇


Java SDK_SDK 下载_对象存储 - 七牛开发者中心


全局处理


通过阅读代码的细节,我们可以发现,我们的方法粒度是单文件的,但事实上,我们可以先将所有的文件遍历一遍,统一进行图片的下载、上传与替换,这样可以节约点时间。


统一替换的逻辑也很简单,我们申明一个全局Map,


private static final Map<String, String> URL_MAP = Maps.newHashMap();

其中,key是旧的新浪图床的链接,value是新的自定义图床的链接。


我们将listAllMDFile这一步中所获取到的所有文件里的所有链接保存于此,下载时只需遍历这个Map的key即可获取到需要下载的图片链接。然后将上传后得到的新链接作为value存在到该Map中即可。


全文替换链接并更新文件


有了上述这些处理步骤,接下来一步就变的异常简单,只需要遍历每个文件,将匹配到全局Map中key的链接替换成Map中的value即可。


/**
* 替换所有的图片链接
*/

private static String replaceUrl(String content, Map<String, String> urlMap) {
for (Map.Entry<String, String> entry : urlMap.entrySet()) {
String oldUrl = entry.getKey();
String newUrl = entry.getValue();
if (StringUtils.isBlank(newUrl)) {
continue;
}
content = RegExUtils.replaceAll(content, oldUrl, newUrl);
}
return content;
}

我们借助commons-lang实现字符串匹配替换,借助Hutools实现文件的读取和写入。


files.forEach(file -> {
try {
FileReader fileReader = new FileReader(file.getPath());
String content = fileReader.readString();
String replaceContent = replaceUrl(content, URL_MAP);
FileWriter writer = new FileWriter(file.getPath());
writer.write(replaceContent);
} catch (Throwable e) {
log.error("write file error, errorMsg:{}", e.getMessage());
}
});

为了安全起见,最好把文件放在新的目录中,不要直接替换掉原来的文件,否则程序出现意外就麻烦了。


接下来我们只需要运行程序,静待备份结果跑完即可。


以上就是本文的全部内容了,希望对你有所帮助


作者:插猹的闰土
来源:juejin.cn/post/7189651446306963514
收起阅读 »

git merge 和 git rebase的区别

git rebase 让你的提交记录更加清晰可读 git rebase 的使用 rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。 如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。 现在我们...
继续阅读 »

git rebase 让你的提交记录更加清晰可读


git rebase 的使用


rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。


如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。



现在我们来用一个例子来解释一下上面的过程。


假设我们现在有2条分支,一个为 master\color{#2196F3}{master} ,一个为 feature/1\color{#2196F3}{feature/1},他们都基于初始的一个提交 add readme\color{#2196F3}{add \ readme} 进行检出分支,之后,master 分支增加了 3.js\color{red}{3.js},和 4.js\color{red}{4.js} 的文件,分别进行了2次提交,feature/1\color{#2196F3}{feature/1} 也增加了 1.js\color{red}{1.js}2.js\color{red}{2.js} 的文件,分别对应以下2条提交记录。


master\color{#2196F3}{master} 分支如下图:



feature/1\color{#2196F3}{feature/1} 分支如下图:



结合起来看是这样的:



此时,切换到 feature/1 分支下,执行 git rebase master ,成功之后,通过 log 查看记录。


如下图所示:可以看到先是逐个应用了 master 分支的更改,然后以 master\color{#2196F3}{master} 分支最后的提交作为基点,再逐个应用 feature/1\color{#2196F3}{feature/1} 的每个更改。



所以,我们的提交记录就会非常清晰,没有分叉,上面演示的是比较顺利的情况,但是大部分情况下,rebase 的过程中会产生冲突的,此时,就需要手动解决冲突,然后使用 git addgit rebase --continue 的方式来处理冲突,完成 rebase,如果不想要某次 rebase 的结果,那么需要使用 git rebase --skip 来跳过这次 rebase


git merge 和 git rebase 的区别


不同于 git rebase的是,git merge 在不是 fast-forward(快速合并)的情况下,会产生一条额外的合并记录,类似 Merge branch 'xxx' into 'xxx' 的一条提交信息。



另外,在解决冲突的时候,用 merge 只需要解决一次冲突即可,简单粗暴,而用 rebase 的时候 ,需要一次又一次的解决冲突。


git rebase 交互模式


在开发中,常会遇到在一个分支上产生了很多的无效的提交,这种情况下使用 rebase 的交互式模式可以把已经发生的多次提交压缩成一次提交,得到了一个干净的提交历史,例如某个分支的提交历史情况如下:



进入交互式模式的方法是执行:


git rebase -i <base-commit>

参数 base-commit 就是指明操作的基点提交对象,基于这个基点进行 rebase 的操作,对于上述提交历史的例子,我们要把最后的一个提交对象 (ac18084\color{#F19E38}{ac18084}) 之前的提交压缩成一次提交,我们需要执行的命令格式是


git rebase -i ac18084

此时会进入一个 vim 的交互式页面,编辑器列出的信息像下列这样。



想要合并这一堆更改,我们要使用 squash 策略进行合并,即把当前的 commit 和它的上一个 commit 内容进行合并, 大概可以表示为下面这样。


pick  ... ...
s ... ...
s ... ...
s ... ...

修改文件后 按下 : 然后 wq 保存退出,此时又会弹出一个编辑页面,这个页面是用来编辑提交的信息,修改为 feat: 更正,最后保存一下,接着使用 git branch 查看提交的 commit 信息,rebase 后的提交记录如下图所示,是不是清爽了很多? rebase 操作可以让我们的提交历史变得更加清晰。




特别注意,只能在自己使用的 feature 分支上进行 rebase 操作,不允许在集成分支上进行 rebase,因为这种操作会修改集成分支的历史记录。



rebase 的风险



patch:【假设本地分支为 dev1,c1 和 c2 是本地往 dev1 分支上做的两次提交】把 dev1 分支上的c1和 c2 “拆”下来,并临时保存成 c1' 和 c2'。git 里将其称为 patch



rebase\color{red}{rebase} 会将当前分支的新提交拆下来,保存成 patch\color{red}{patch},然后合并进其他分支新的 commit\color{red}{commit},最后将 patch\color{red}{patch} 接进当前分支。这是 rebase\color{red}{rebase} 对多条分支的操作。对于单条分支,rebase\color{red}{rebase} 还能够合并多个 commit\color{red}{commit} 单号,将多个提交合并成一个提交。


git rebase -i [commit id]命令能够合并(整改) commit id 之前的所有 commit\color{red}{commit} 单。加上-i选项能够提供一个交互界面,分阶段修改commit信息并 rebase\color{red}{rebase}


但这里就会出现一个问题:如果你合并多个单号时,一不小心合并多了,将别人的提交也合并了,此时你本地的 commit history\color{red}{commit \ history} 和远程仓库的 commit history\color{red}{commit \ history} 不一样了,无论你如何 push\color{red}{push},都无法推送你的代码了。如果你并不记得 rebase\color{red}{rebase} 之前的 HEAD\color{red}{HEAD} 指向的 commit\color{red}{commit}commit ID\color{red}{commit \ ID} 的话,git reflog\color{red}{git \ reflog} 都救不了你。


tips:  你可以 push\color{red}{push} 时带上 f\color{red}{-f} 参数,强制覆盖远程 commit history\color{red}{commit \ history},你这样做估计会被打,因为覆盖之后,团队的其他人的本地 commit history\color{red}{commit \ history} 就与远程的不一样了,都无法推送了。


因此,请保证仅仅对自己私有的提交单进行 rebase\color{red}{rebase} 操作,对于已经合并进远程仓库的历史提交单,不要使用 rebase\color{red}{rebase} 操作合并 commit\color{red}{commit} 单。


作者:d_motivation
来源:juejin.cn/post/7277089907974357052
收起阅读 »

第一次使用缓存,因为没预热,翻车了

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。 悲惨的上线时刻 事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状...
继续阅读 »

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。


悲惨的上线时刻


事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状态,提单时也需要校验库存状态是否可售卖。但是由于库存状态的计算包含较复杂的业务逻辑,耗时比较高,在500ms以上。如果要在商品页面透出库存状态那么商品页面耗时增加500ms,这几乎是无法忍受的事情。


如何实现呢?最合适的方案当然是缓存了,我当时设计的方案是如果缓存有库存状态直接读缓存,如果缓存查不到,则计算库存状态,然后加载进缓存,同时设定过期时间。何时写库存呢? 答案是过期后,cache miss时重新加载进缓存。 由于计算逻辑较复杂,库存扣减等用户写操作没有同步更新缓存,但是产品认可库存状态可以有几分钟的状态不一致。为什么呢?


因为仓库有冗余库存,就算库存状态不一致导致超卖,也能容忍。同时库存不足以后,需要运营补充库存,而补充库存的时间是肯定比较长的。虽然补充库存完成几分钟后,才变为可售卖的,产品也能接受。 梳理完缓存的读写方案,我就沉浸于学习Redis的过程。


第一次使用缓存,我把时间和精力都放在Redis存储结构,Redis命令,Redis为什么那么快等方面的关注。如饥似渴的学习Redis知识。


直到上线阶段我也没有意识到系统设计的缺陷。


代码写的很快,测试验证也没有问题。然而上线过程中,就开始噼里啪啦的报警,开始我并没有想到报警这事和我有关。直到有人问我,“XXX,你是不是在上线库存状态的需求?”。


我人麻了,”怎么了,啥事”,我颤抖的问


“商品页面耗时暴涨,赶紧回滚”。一个声音传来


“我草”,那一瞬间,我的血压上涌,手心发痒,心跳加速,头皮发麻,颤抖的手不知道怎么在发布系统点回滚,“我没回滚过啊,咋回滚啊?”


“有降级开关吗”? 一个声音传来。


"没写..."。我回答的时候觉得自己真是二笔,为啥没加降级啊。(这也是复盘被骂的重要原因)


那么如何对缓存进行预热呢?


如何预热缓存


灰度放量


灰度放量实际上并不是缓存预热的办法,但是确实能避免缓存雪崩的问题。例如这个需求场景中,如果我没有放开全量数据,而是选择放量1%的流量。这样系统的性能不会有较大的下降,并且逐步放量到100%。


虽然这个过程中,没有主动同步数据到缓存,但是通过控制放量的节奏,保证了初始化缓存过程中,不会出现较大的耗时波动。


例如新上线的缓存逻辑,可以考虑逐渐灰度放量。


扫描数据库刷缓存


如果缓存维度是商品维度或者用户维度,可以考虑扫描数据库,提前预热部分数据到缓存中。


开发成本较高。除了开发缓存部分的代码,还需要开发扫描全表的任务。为了控制缓存刷新的进度,还需要使用线程池增加并发,使用限流器限制并发。这个方案的开发成本较高。


通过数据平台刷缓存


这是比较好的方式,具体怎么实现呢?


数据平台如果支持将数据库离线数据同步到Hive,Hive数据同步到Kafka,我们就可以编写Hive SQL,建立ETL任务。把业务需要被刷新的数据同步到Kafka中,再消费Kafka,把数据写入到缓存中。在这个过程中通过数据平台控制并发度,通过Kafka 分片和消费线程并发度控制 缓存写入的速率。


这个方案开发逻辑包括ETL 任务,消费Kafka写入缓存。这两部分的开发工作量不大。并且相比扫描全表任务,ETL可以编写更加复杂的SQL,修改后立即上线,无需自己控制并发、控制限流。在多个方面ETL刷缓存效率更高。


但是这个方案需要公司级别支持 多个存储系统之间可以进行数据同步。例如mysql、kafka、hive等。


除了首次上线,是否还有其他场景需要预热缓存呢?


需要预热缓存的其他场景


如果Redis挂了,数据怎么办


刚才提到上线前,一定要进行缓存预热。还有一个场景:假设Redis挂了,怎么办?全量的缓存数据都没有了,全部请求同时打到数据库,怎么办。


除了首次上线需要预热缓存,实际上如果缓存数据丢失后,也需要预热缓存。所以预热缓存的任务一定要开发的,一方面是上线前预热缓存,同时也是为了保证缓存挂掉后,也能重新预热缓存。


假如有大量数据冷启动怎么办


假如促销场景,例如春节抢红包,平时非活跃用户会在某个时间点大量打开App,这也会导致大量cache miss,进而导致雪崩。 此时就需要提前预热缓存了。具体的办法,可以考虑使用ETL任务。离线加载大量数据到Kafka,然后再同步到缓存。


总结



  1. 一定要预热缓存,不然线上接口性能和数据库真的扛不住。

  2. 可以通过灰度放量,扫描全表、ETL数据同步等方式预热缓存

  3. Redis挂了,大量用户冷启动的促销场景等场景都需要提前预热缓存。


作者:他是程序员
来源:juejin.cn/post/7277461864349777972
收起阅读 »

王兴入局大模型!美团耗资21亿拿下光年之外100%股权

【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。**** 官宣了!美团以20.65亿人民币收购光年之外。 就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。 总代价包括现金233,673,600美元;债务承担人民币366,...
继续阅读 »
【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。****

官宣了!美团以20.65亿人民币收购光年之外。


就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。


总代价包括现金233,673,600美元;债务承担人民币366,924,000元;现金人民币1.00元。




于公告日期,光年之外的净现金总额约为285,035,563美元。转让协议交割完成后,美团将持有光年之外100%权益。


前几天,光年之外联合创始人王慧文因健康问题暂时离岗引发许多人的关注。


甚至,外界关心诸多的是他的停职对公司造成哪些影响。




美团在公告中对于并购的解释是,通过收购事项获得领先的AGI技术及人才,有机会加强其于快速增长的人工智能行业中的竞争力。


这次,美团出手,意味着光年之外在后续运营有了足够资金支持。


同时,对美团来说,大模型能对未来业务转型也将产生有利帮助。


美团拿下光年之外



其实,在外界看来,美团收购光年之外,就像是板凳钉钉的事。


从感性层面讲,王兴与王慧文是清华的室友,在创业路上并肩作战。王慧文入局大模型后,王兴紧接着应声跟进。


在大模型爆火后,美团CEO王兴也对此表示极大的关注,甚至,在3月份还投资光年之外。


当时,王兴表示「AI大模型让我既兴奋于即将创造出来的巨大生产力,又忧虑它未来对整个世界的冲击。老王和在创业路上同行近二十年,既然他决心拥抱这次大浪潮,那我必须支持。」




从理性层面讲,自2019年美团将战略升级为「零售+科技」后,不论是王兴本人,还是公司来讲,对AI也投入非常大的兴趣。


当前,大模型已经成为兵家必争之地,国内许多头部科技纷纷入局。


据「豹变」独家报道,美团做大模型,已经有2个多月,几乎是与王兴投资光年之外同步进行的。


据称,算法团队正积极扩招,甚至还在筹划成立单独的「平台部门」,帮助美团大模型通过具体的商业化形式落地。


对美团来讲,智能配送系统、外卖无人车等场景,都需要AI驱动。


收购光年之外后,美团能够将大模型的能力,与自家核心业务相结合,比如外卖、本地生活服务等等。




此外,还能够在客服、物流、产品体验等各种场景中实现应用,将大模型能力与场景深度融合。


美团方面表示,并购完成后,将支持光年团队继续在大模型领域进行探索和研究。


所以说,美团的未来还是值得期待的。


而前几日,王慧文病倒的消息,让外界猜测纷纷,比如融资不顺利,或团队组建困难。


有国内媒体澄清道,光年之外A轮融资已经完成一个月,融资到账实际金额远高于外部报道的2.3亿美元,网传的“融资不顺利”消息,属于谣言。


此次美团在港交所的公告,也证实了这一点。


而在人才组队上,光年之外也进展顺利。


在成立后不久,光年之外就以换股形式收购了一流科技,原核心技术团队被保留。


而在两个月内,光年之外的研发团队规模就已经在70人左右,团队在算法等领域,研发经验丰富。


这样一支已经组建成熟的团队,在当下的大模型之战中,无疑属于稀缺资源。


美团选择收购光年之外,显然也是经过深思熟虑。


VC平稳退出



光年之外在6月初刚刚完成了的这笔2.3亿美元的融资,由源码资本领投。


腾讯、五源资本和快手创始人宿华也参与了这次融资。


从港交所披露的信息来看,除了6月初的这轮融资,红杉中国也在前期对光年之外进行了投资。


当王慧文因病离开光年之外的领导岗位之后,这些前期投资的VC都因为这突发的黑天鹅事件,可能面临着投资打水漂的风险。


但是随着美团的出手收购,这些参与光年之外的投资至少能在一定程度上落袋为安。


不用再担心因为被投公司核心创始人离职给被投公司带来的巨大影响。


王慧文辞任董事



此前,大模型创业4个月,王慧文就因身体原因停职休养。


紧接着,美团在港交所公布,王慧文已提出辞去公司非执行董事、公司董事会提名委员会成员和公司授权代表职务,自6月26日起生效。




在王慧文卸任董事后,美团宣布,执行董事穆荣均已获委任为授权代表,自2023年6月26日起生效。


另外,提名委员会将由冷雪松先生和沈向洋博士组成,冷雪松继续担任提名委员会主席。


在另一份公告中,美团更新了董事会成员。王兴和穆荣均担任公司执行董事,沈南鹏为非执行董事,欧高敦、冷雪松、沈向洋是独立非执行董事。



参考资料:


www1.hkexnews.hk/listedco/li…


作者:新智元
链接:https://juejin.cn/post/7253434752836632613
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

开发一个网站,用户密码你打算怎么存储

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。 1....
继续阅读 »

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。


image.png


1. 如何安全地传输用户的密码


要拒绝用户密码在网络上裸奔,我们很容易就想到使用https协议,那先来回顾下https相关知识吧~


1.1 https 协议


image.png



  • 「http的三大风险」


为什么要使用https协议呢?http它不香吗? 因为http是明文信息传输的。如果在茫茫的网络海洋,使用http协议,有以下三大风险:




  • 窃听/嗅探风险:第三方可以截获通信数据。

  • 数据篡改风险:第三方获取到通信数据后,会进行恶意修改。

  • 身份伪造风险:第三方可以冒充他人身份参与通信。



如果传输不重要的信息还好,但是传输用户密码这些敏感信息,那可不得了。所以一般都要使用https协议传输用户密码信息。



  • 「https 原理」


https原理是什么呢?为什么它能解决http的三大风险呢?



https = http + SSL/TLS, SSL/TLS 是传输层加密协议,它提供内容加密、身份认证、数据完整性校验,以解决数据传输的安全性问题。



为了加深https原理的理解,我们一起复习一下 一次完整https的请求流程吧~


image.png




  1. 客户端发起https请求

  2. 服务器必须要有一套数字证书,可以自己制作,也可以向权威机构申请。这套证书其实就是一对公私钥。

  3. 服务器将自己的数字证书(含有公钥、证书的颁发机构等)发送给客户端。

  4. 客户端收到服务器端的数字证书之后,会对其进行验证,主要验证公钥是否有效,比如颁发机构,过期时间等等。如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密算法的密钥,其实是一个随机值),并且用证书的公钥对这个随机值加密。

  5. 客户端会发起https中的第二个请求,将加密之后的客户端密钥(随机值)发送给服务器。

  6. 服务器接收到客户端发来的密钥之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。

  7. 服务器将加密后的密文返回给客户端。

  8. 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。




  • 「https一定安全吗?」


https的数据传输过程,数据都是密文的,那么,使用了https协议传输密码信息,一定是安全的吗?其实不然




  • 比如,https 完全就是建立在证书可信的基础上的呢。但是如果遇到中间人伪造证书,一旦客户端通过验证,安全性顿时就没了哦!平时各种钓鱼不可描述的网站,很可能就是黑客在诱导用户安装它们的伪造证书!

  • 通过伪造证书,https也是可能被抓包的哦。



1.2 对称加密算法


既然使用了https协议传输用户密码,还是 「不一定安全」,那么,我们就给用户密码 「加密再传输」 呗~


加密算法有 「对称加密」「非对称加密」 两大类。用哪种类型的加密算法 「靠谱」 呢?



对称加密:加密和解密使用 「相同密钥」 的加密算法。



image.png
常用的对称加密算法主要有以下几种哈:


image.png
如果使用对称加密算法,需要考虑 「密钥如何给到对方」 ,如果密钥还是网络传输给对方,传输过程,被中间人拿到的话,也是有风险的哦。


1.3 非对称加密算法


再考虑一下非对称加密算法呢?



「非对称加密:」 非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。



image.png


常用的非对称加密算法主要有以下几种哈:


image.png



如果使用非对称加密算法,也需要考虑 「密钥公钥如何给到对方」 ,如果公钥还是网络传输给对方,传输过程,被中间人拿到的话,会有什么问题呢?「他们是不是可以伪造公钥,把伪造的公钥给客户端,然后,用自己的私钥等公钥加密的数据过来?」 大家可以思考下这个问题哈~



我们直接 「登录一下百度」 ,抓下接口请求,验证一发大厂是怎么加密的。可以发现有获取公钥接口,如下:


image.png
再看下登录接口,发现就是RSA算法,RSA就是 「非对称加密算法」 。其实百度前端是用了JavaScript库 「jsencrypt」 ,在github的star还挺多的。


image.png
因此,我们可以用 「https + 非对称加密算法(如RSA)」 传输用户密码~


2. 如何安全地存储你的密码?


假设密码已经安全到达服务端啦,那么,如何存储用户的密码呢?一定不能明文存储密码到数据库哦!可以用 「哈希摘要算法加密密码」 ,再保存到数据库。



哈希摘要算法:只能从明文生成一个对应的哈希值,不能反过来根据哈希值得到对应的明文。



2.1  MD5摘要算法保护你的密码


MD5 是一种非常经典的哈希摘要算法,被广泛应用于数据完整性校验、数据(消息)摘要、数据加密等。但是仅仅使用 MD5 对密码进行摘要,并不安全。我们看个例子,如下:


public class MD5Test {  
    public static void main(String[] args) {
        String password = "abc123456";
        System.out.println(DigestUtils.md5Hex(password));
    }
}

运行结果:
0659c7992e268962384eb17fafe88364


在MD5免费破解网站一输入,马上就可以看到原密码了。。。


image.png
试想一下,如果黑客构建一个超大的数据库,把所有20位数字以内的数字和字母组合的密码全部计算MD5哈希值出来,并且把密码和它们对应的哈希值存到里面去(这就是 「彩虹表」 )。在破解密码的时候,只需要查一下这个彩虹表就完事了。所以 「单单MD5对密码取哈希值存储」 ,已经不安全啦~


2.2  MD5+盐摘要算法保护用户的密码


那么,为什么不试一下MD5+盐呢?什么是 「加盐」



在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。



用户密码+盐之后,进行哈希散列,再保存到数据库。这样可以有效应对彩虹表破解法。但是呢,使用加盐,需要注意一下几点:




  • 不能在代码中写死盐,且盐需要有一定的长度(盐写死太简单的话,黑客可能注册几个账号反推出来)

  • 每一个密码都有独立的盐,并且盐要长一点,比如超过 20 位。(盐太短,加上原始密码太短,容易破解)

  • 最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。



2.3 提升密码存储安全的利器登场,Bcrypt


即使是加了盐,密码仍有可能被暴力破解。因此,我们可以采取更 「慢一点」 的算法,让黑客破解密码付出更大的代价,甚至迫使他们放弃。提升密码存储安全的利器~Bcrypt,可以闪亮登场啦。



实际上,Spring Security 已经废弃了 MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt 生而为保存密码设计的算法,相比 MD5 要慢很多。



看个例子对比一下吧:


public class BCryptTest {  

    public static void main(String[] args) {
        String password = "123456";
        long md5Begin = System.currentTimeMillis();
        DigestUtils.md5Hex(password);
        long md5End = System.currentTimeMillis();
        System.out.println("md5 time:"+(md5End - md5Begin));
        long bcrytBegin = System.currentTimeMillis();
        BCrypt.hashpw(password, BCrypt.gensalt(10));
        long bcrytEnd = System.currentTimeMillis();
        System.out.println("bcrypt Time:" + (bcrytEnd- bcrytBegin));
    }
}

运行结果:


md5 time:47


bcrypt Time:1597


粗略对比发现,BCrypt比MD5慢几十倍,黑客想暴力破解的话,就需要花费几十倍的代价。因此一般情况,建议使用Bcrypt来存储用户的密码


3. 总结



  • 因此,一般使用https 协议 + 非对称加密算法(如RSA)来传输用户密码,为了更加安全,可以在前端构造一下随机因子哦。

  • 使用BCrypt + 盐存储用户密码。

  • 在感知到暴力破解危害的时候,「开启短信验证、图形验证码、账号暂时锁定」 等防御机制来抵御暴力破解。


作者:小王和八蛋
来源:juejin.cn/post/7260140790546251831
收起阅读 »