分类 技术 下的文章 - 第 6 页 - 酷游博客
首页
关于
友链
Search
1
阿里的简历多久可以投递一次?次数多了有没有影响?可以同时进行吗?
45 阅读
2
Java中泛型的理解
40 阅读
3
Java 14 发布了,再也不怕 NullPointerException 了!
38 阅读
4
Java中的可变参数
37 阅读
5
该如何创建字符串,使用" "还是构造函数?
29 阅读
技术
登录
/
注册
找到
556
篇与
技术
相关的结果
- 第 6 页
2025-01-22
密码保护:模拟面试第2期——10+年工作经验,目前是架构师,聊的东西很多,知识面很广。
此内容受密码保护。如需查阅,请在下列字段中输入您的密码。 密码:
技术
# 未分类
酷游
1月22日
0
7
0
2025-01-22
求求你,不要再使用!=null判空了!
本文来自作者投稿,原作者:上帝爱吃苹果 对于Java程序员来说,null是令人头痛的东西。时常会受到空指针异常(NPE)的骚扰。连Java的发明者都承认这是他的一项巨大失误。 那么,有什么办法可以避免在代码中写大量的判空语句呢? 有人说可以使用 JDK8提供的 Optional 来避免判空,但是用起来还是有些麻烦。 作者在日常工作中,封装了一个工具,可以可以链式调用对象成员而无需判空,相比原有的if null逻辑 和 JDK8提供的 Optional 更加优雅易用,在工程实践中大大提高了编码效率,也让代码更加的精准和优雅。 不优雅的判空调用 我想从事Java开发的小伙伴肯定有遇到过下面这种让人难受的判空逻辑: 现在有一个User类,School 是它的成员变量 /** * @author Axin * @since 2020-09-20 * @summary 一个User类定义 * (Ps:Data 是lombok组件提供的注解,简化了get set等等的约定代码) */ @Data public class User { private String name; private String gender; private School school; @Data public static class School { private String scName; private String adress; } } 现在想要获得School的成员变量 adress , 一般的处理方式: public static void main(String[] args) { User axin = new User(); User.School school = new User.School(); axin.setName("hello"); if (Objects.nonNull(axin) && Objects.nonNull(axin.getSchool())) { User.School userSc = axin.getSchool(); System.out.println(userSc.getAdress()); } } 获取adress时要对School进行判空,虽然有些麻烦,到也能用,通过 JDK8 提供的 Optional 工具也是可以,但还是有些麻烦。 而下文的 OptionalBean 提供一种可以链式不断地调用成员变量而无需判空的方法,直接链式调用到你想要获取的目标变量,而无需担心空指针的问题。 链式调用成员变量 如果用了本文设计的工具 OptionalBean ,那么上述的调用可以简化成这样: public static void main(String[] args) { User axin = new User(); User.School school = new User.School(); axin.setName("hello"); // 1. 基本调用 String value1 = OptionalBean.ofNullable(axin) .getBean(User::getSchool) .getBean(User.School::getAdress).get(); System.out.println(value1); } 执行结果: 其中User的school变量为空,可以看到代码并没有空指针,而是返回了null。这个工具怎么实现的呢? OptionalBean 工具 /** * @author Axin * @since 2020-09-10 * @summary 链式调用 bean 中 value 的方法 */ public final class OptionalBean { private static final OptionalBean EMPTY = new OptionalBean(); private final T value; private OptionalBean() { this.value = null; } /** * 空值会抛出空指针 * @param value */ private OptionalBean(T value) { this.value = Objects.requireNonNull(value); } /** * 包装一个不能为空的 bean * @param value * @param * @return */ public static OptionalBean of(T value) { return new OptionalBean(value); } /** * 包装一个可能为空的 bean * @param value * @param * @return */ public static OptionalBean ofNullable(T value) { return value == null ? empty() : of(value); } /** * 取出具体的值 * @param fn * @param * @return */ public T get() { return Objects.isNull(value) ? null : value; } /** * 取出一个可能为空的对象 * @param fn * @param * @return */ public OptionalBean getBean(Function
技术
# Java
酷游
1月22日
0
8
0
2025-01-22
Java 14 发布了,不使用"class"也能定义类了?还顺手要干掉Lombok!
2020年3月17日发布,Java正式发布了JDK 14 ,目前已经可以开放下载。在JDK 14中,共有16个新特性,本文主要来介绍其中的一个特性:JEP 359: Records 官方吐槽最为致命 早在2019年2月份,Java 语言架构师 Brian Goetz,曾经写过一篇文章(http://cr.openjdk.java.net/~briangoetz/amber/datum.html ),详尽的说明了并吐槽了Java语言,他和很多程序员一样抱怨“Java太啰嗦”或有太多的“繁文缛节”,他提到:开发人员想要创建纯数据载体类(plain data carriers)通常都必须编写大量低价值、重复的、容易出错的代码。如:构造函数、getter/setter、equals()、hashCode()以及toString()等。 以至于很多人选择使用IDE的功能来自动生成这些代码。还有一些开发会选择使用一些第三方类库,如Lombok等来生成这些方法,从而会导致了令人吃惊的表现(surprising behavior)和糟糕的可调试性(poor debuggability)。 那么,Brian Goetz 大神提到的纯数据载体到底指的是什么呢。他举了一个简单的例子: final class Point { public final int x; public final int y; public Point(int x, int y) { this.x = x; this.y = y; } // state-based implementations of equals, hashCode, toString // nothing else } 这里面的Piont其实就是一个纯数据载体,他表示一个”点”中包含x坐标和y坐标,并且只提供了构造函数,以及一些equals、hashCode等方法。 于是,BrianGoetz大神提出一种想法,他提到,Java完全可以对于这种纯数据载体通过另外一种方式表示。 其实在其他的面向对象语言中,早就针对这种纯数据载体有单独的定义了,如Scala中的case、Kotlin中的data以及C#中的record。这些定义,尽管在语义上有所不同,但是它们的共同点是类的部分或全部状态可以直接在类头中描述,并且这个类中只包含了纯数据而已。 于是,他提出Java中是不是也可以通过如下方式定义一个纯数据载体呢? record Point(int x, int y) 神说要用record,于是就有了 就像大神吐槽的那样,我们通常需要编写大量代码才能使类变得有用。如以下内容: toString()方法 hashCode() and equals()方法 Getter 方法 一个共有的构造函数 对于这种简单的类,这些方法通常是无聊的、重复的,而且是可以很容易地机械地生成的那种东西(ide通常提供这种功能)。 当你阅读别人的代码时,可能会更加头大。例如,别人可能使用IDE生成的hashCode()和equals()来处理类的所有字段,但是如何才能在不检查实现的每一行的情况下确定他写的对呢?如果在重构过程中添加了字段而没有重新生成方法,会发生什么情况呢? 大神Brian Goetz提出了使用record定义一个纯数据载体的想法,于是,Java 14 中便包含了一个新特性:EP 359: Records ,作者正是 Brian Goetz  Records的目标是扩展Java语言语法,Records为声明类提供了一种紧凑的语法,用于创建一种类中是“字段,只是字段,除了字段什么都没有”的类。通过对类做这样的声明,编译器可以通过自动创建所有方法并让所有字段参与hashCode()等方法。这是JDK 14中的一个预览特性。 一言不合反编译 Records的用法比较简单,和定义Java类一样: record Person (String firstName, String lastName) {} 如上,我们定义了一个Person记录,其中包含两个组件:firstName和lastName,以及一个空的类体。 那么,这个东西看上去也是个语法糖,那他到底是怎么实现的那? 我们先尝试对他进行编译,记得使用--enable-preview参数,因为records功能目前在JDK 14中还是一个预览(preview)功能。 > javac --enable-preview --release 14 Person.java Note: Person.java uses preview language features. Note: Recompile with -Xlint:preview for details. 如前所述,Record只是一个类,其目的是保存和公开数据。让我们看看用javap进行反编译,将会得到以下代码: public final class Person extends java.lang.Record { private final String firstName; private final String lastName; public Person(java.lang.String, java.lang.String); public java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public java.lang.String firstName(); public java.lang.String lastName(); } 通过反编译得到的类,我们可以得到以下信息: 1、生成了一个final类型的Person类(class),说明这个类不能再有子类了。 2、这个类继承了java.lang.Record类,这个我们使用enum创建出来的枚举都默认继承java.lang.Enum有点类似 3、类中有两个private final 类型的属性。所以,record定义的类中的属性都应该是private final类型的。 4、有一个public的构造函数,入参就是两个主要的属性。如果通过字节码查看其方法体的话,其内容就是以下代码,你一定很熟悉: public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } 5、有两个getter方法,分别叫做firstName和lastName。这和JavaBean中定义的命名方式有区别,或许大神想通过这种方式告诉我们record定义出来的并不是一个JavaBean吧。 6、还帮我们自动生成了toString(), hashCode() 和 equals()方法。值得一提的是,这三个方法依赖invokedynamic来动态调用包含隐式实现的适当方法。 还可以这样玩 前面的例子中,我们简单的创建了一个record,那么,record中还能有其他的成员变量和方法吗?我们来看下。 1、我们不能将实例字段添加到record中。但是,我们可以添加静态字段。 record Person (String firstName, String lastName) { static int x; } 2、我们可以定义静态方法和实例方法,可以操作对象的状态。 record Person (String firstName, String lastName) { static int x; public static void doX(){ x++; } public String getFullName(){ return firstName + " " + lastName; } } 3、我们还可以添加构造函数。 record Person (String firstName, String lastName) { static int x; public Person{ if(firstName == null){ throw new IllegalArgumentException( "firstName can not be null !"); } } public Person(String fullName){ this(fullName.split(" ")[0],this(fullName.split(" ")[1]) } } 所以,我们是可以在record中添加静态字段/方法的,但是问题是,我们应该这么做吗? 请记住,record推出背后的目标是使开发人员能够将相关字段作为单个不可变数据项组合在一起,而不需要编写冗长的代码。这意味着,每当您想要向您的记录添加更多的字段/方法时,请考虑是否应该使用完整的类来代替它。 总结 record 解决了使用类作为数据包装器的一个常见问题。纯数据类从几行代码显著地简化为一行代码。 但是,record目前是一种预览语言特性,这意味着,尽管它已经完全实现,但在JDK中还没有标准化。 那么问题来了,如果你用上了Java 14之后,你还会使用Lombok吗?哦不,你可能短时间内都用不上,因为你可能Java 8都还没用熟~ 参考资料: https://openjdk.java.net/jeps/359 https://dzone.com/articles/a-first-look-at-records-in-java-14 https://aboullaite.me/java-14-records/ http://cr.openjdk.java.net/~briangoetz/amber/datum.html
技术
# Java
酷游
1月22日
0
5
0
2025-01-22
Google Guava 用户指南 ---- 初始Guava类库
原文地址 翻译地址 | 翻译:HollisChuang 转载请注明出处。 Guava项目包含多个基于Java基础的核心类库:collections(集合), caching(缓存), primitives support(原生类型支持 ), concurrency libraries(并发类库), common annotations(通用注解), string processing(字符串处理), I/O等。这些工具被google的开发者们广泛应用在各类产品中。 一般来说,通过JavaDoc并不是学习使用这些类库的最好方式。所以,我们试着通过一些可读性较高并且有趣的解释来帮助开发者了解Guava的特性。 该文档内容在不断完善中。 基本工具(Basic utilities) 让使用Java开发变得更加愉快。 使用并避免null: null是很模棱两可的,很多时候会导致令人疑惑的的错误,这让开发人员感到很不舒服。导致很多类似问题的原因都是因为盲目的接受null值。Guava基本工具在处理null的时候一般不会盲目的接受,而是采用拒绝或者快速失败(fail-fast)的方式处理。 前置条件: 置条件使你的方法更加简单 常用的Object对象方法: 简化Object方法方法的实现,比如toString()和hashCode()方法。 排序: Guava有强大的“流畅的比较器”类。 异常处理: 简化异常和错误的检查和传递 集合(Collections) Guava对JDK的集合做了扩展,这部分也是Guava中最成熟和被众所周知的。 不可变集合 可以用作常量的集合,不仅可以进行防御性编程还能提高性能。 新集合类型 实现了一些jdk本身并提供的新集合类型:multisets(多重集), multimaps(多重映射), tables(表),bidirectional maps(双向映射)等。 强大的工具集合, 提供了一些java.util.Collections中不提供的常用操作。 扩展工具 很轻易的创建集合的装饰器,或实现迭代器 缓存(Caches) 很实用的本地缓存,并支持各种各样的失效策略。 函数式风格(Functional idioms) 可以显著简化代码,但请谨慎使用 并发(Concurrency) 强大并且简单的抽象,让编写正确的并发代码更简单 ListenableFuture 完成后触发回调的Future 服务 帮你接管并控制一些复杂的状态逻辑的开始和结局。 字符串处理(String) 提供几个非常有用的字符串工具:分割,连接,填充等 原生类型(Primitives) 支持一些jdk并不提供的对基本类型(如int char,包括某些类型的无符号(unsined)形式)的操作 区间(Ranges) Guava对可比较的类型提供了强大的API来处理范围。包括连续和离散类型。 I / O 简化的I/O操作,特别是针对java5和java6的流和文件的I/O操作。 散列(Hashing) 提供比Object.hashCode()更复杂的散列实现,并提供布隆过滤器(Bloom Filter)的实现 事件总线(EventBus) 采用发布-订阅模式进行组件之间的通信,而无需显式地注册。 数学运算(Math) 提供了JDK中并不提供的优化的、经过充分测试的数学工具类 反射(Reflection) 反射机制工具类
技术
# guava
酷游
1月22日
0
16
0
2025-01-22
再有人问你如何实现订单到期关闭,就把这篇文章发给他!
在电商、支付等系统中,一般都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类似的场景有很多,还有比如到期自动收货、超时自动退款、下单后自动发送短信等等都是类似的业务问题。 本文就从这样的业务问题出发,探讨一下都有哪些技术方案,这些方案的实现细节,以及相关的优缺点都有什么? 因为本文要讲的内容比较多,涉及到11种具体方案,受篇幅限制,这篇文章主要是讲方案,不会涉及到具体的代码实现。 因为只要方案搞清楚了,代码实现不是难事儿。 一、被动关闭 在解决这类问题的时候,有一种比较简单的方式,那就是通过业务上的被动方式来进行关单操作。 简单点说,就是订单创建好了之后。我们系统上不做主动关单,什么时候用户来访问这个订单了,再去判断时间是不是超过了过期时间,如果过了时间那就进行关单操作,然后再提示用户。 这种做法是最简单的,基本不需要开发定时关闭的功能,但是他的缺点也很明显,那就是如果用户一直不来查看这个订单,那么就会有很多脏数据冗余在数据库中一直无法被关单。 还有一个缺点,那就是需要在用户的查询过程中进行写的操作,一般写操作都会比读操作耗时更长,而且有失败的可能,一旦关单失败了,就会导致系统处理起来比较复杂。 所以,这种方案只适合于自己学习的时候用,任何商业网站中都不建议使用这种方案来实现订单关闭的功能。 二、定时任务 定时任务关闭订单,这是很容易想到的一种方案。 具体实现细节就是我们通过一些调度平台来实现定时执行任务,任务就是去扫描所有到期的订单,然后执行关单动作。 这个方案的优点也是比较简单,实现起来很容易,基于Timer、ScheduledThreadPoolExecutor、或者像xxl-job这类调度框架都能实现,但是有以下几个问题: 1、时间不精准。 一般定时任务基于固定的频率、按照时间定时执行的,那么就可能会发生很多订单已经到了超时时间,但是定时任务的调度时间还没到,那么就会导致这些订单的实际关闭时间要比应该关闭的时间晚一些。 2、无法处理大订单量。 定时任务的方式是会把本来比较分散的关闭时间集中到任务调度的那一段时间,如果订单量比较大的话,那么就可能导致任务执行时间很长,整个任务的时间越长,订单被扫描到时间可能就很晚,那么就会导致关闭时间更晚。 3、对数据库造成压力。 定时任务集中扫表,这会使得数据库IO在短时间内被大量占用和消耗,如果没有做好隔离,并且业务量比较大的话,就可能会影响到线上的正常业务。 4、分库分表问题。 订单系统,一旦订单量大就可能会考虑分库分表,在分库分表中进行全表扫描,这是一个极不推荐的方案。 所以,定时任务的方案,适合于对时间精确度要求不高、并且业务量不是很大的场景中。如果对时间精度要求比较高,并且业务量很大的话,这种方案不适用。 三、JDK自带的DelayQueue 有这样一种方案,他不需要借助任何外部的资源,直接基于应用自身就能实现,那就是基于JDK自带的DelayQueue来实现。 DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。 基于延迟队列,是可以实现订单的延迟关闭的,首先,在用户创建订单的时候,把订单加入到DelayQueue中,然后,还需要一个常驻任务不断的从队列中取出那些到了超时时间的订单,然后在把他们进行关单,之后再从队列中删除掉。 这个方案需要有一个线程,不断的从队列中取出需要关单的订单。一般在这个线程中需要加一个while(true)循环,这样才能确保任务不断的执行并且能够及时的取出超时订单。 使用DelayQueue实现超时关单的方案,实现起来简单,不须要依赖第三方的框架和类库,JDK原生就支持了。 当然这个方案也不是没有缺点的,首先,基于DelayQueue的话,需要把订单放进去,那如果订单量太大的话,可能会导致OOM的问题;另外,DelayQueue是基于JVM内存的,一旦机器重启了,里面的数据就都没有了。虽然我们可以配合数据库的持久化一起使用。而且现在很多应用都是集群部署的,那么集群中多个实例上的多个DelayQueue如何配合是一个很大的问题。 所以,基于JDK的DelayQueue方案只适合在单机场景、并且数据量不大的场景中使用,如果涉及到分布式场景,那还是不建议使用。 四、Netty的时间轮 还有一种方式,和上面我们提到的JDK自带的DelayQueue类似的方式,那就是基于时间轮实现。 为什么要有时间轮呢?主要是因为DelayQueue插入和删除操作的平均时间复杂度——O(nlog(n)),虽然已经挺好的了,但是时间轮的方案可以将插入和删除操作的时间复杂度都降为O(1)。 时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。 基于Netty的HashedWheelTimer可以帮助我们快速的实现一个时间轮,这种方式和DelayQueue类似,缺点都是基于内存、集群扩展麻烦、内存有限制等等。 但是他相比DelayQueue的话,效率更高一些,任务触发的延迟更低。代码实现上面也更加精简。 所以,基于Netty的时间轮方案比基于JDK的DelayQueue效率更高,实现起来更简单,但是同样的,只适合在单机场景、并且数据量不大的场景中使用,如果涉及到分布式场景,那还是不建议使用。 五、Kafka的时间轮 既然基于Netty的时间轮存在一些问题,那么有没有其他的时间轮的实现呢? 还真有的,那就是Kafka的时间轮,Kafka内部有很多延时性的操作,如延时生产,延时拉取,延时数据删除等,这些延时功能由内部的延时操作管理器来做专门的处理,其底层是采用时间轮实现的。 而且,为了解决有一些时间跨度大的延时任务,Kafka 还引入了层级时间轮,能更好控制时间粒度,可以应对更加复杂的定时任务处理场景; Kafka 中的时间轮的实现是 TimingWheel 类,位于 kafka.utils.timer 包中。基于Kafka的时间轮同样可以得到O(1)时间复杂度,性能上还是不错的。 基于Kafka的时间轮的实现方式,在实现方式上有点复杂,需要依赖kafka,但是他的稳定性和性能都要更高一些,而且适合用在分布式场景中。 六、RocketMQ延迟消息 相比于Kafka来说,RocketMQ中有一个强大的功能,那就是支持延迟消息。 延迟消息,当消息写入到Broker后,不会立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息。 有了延迟消息,我们就可以在订单创建好之后,发送一个延迟消息,比如20分钟取消订单,那就发一个延迟20分钟的延迟消息,然后在20分钟之后,消息就会被消费者消费,消费者在接收到消息之后,去关单就行了。 但是,RocketMQ的延迟消息并不是支持任意时长的延迟的,它只支持:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h这几个时长。(商业版支持任意时长) 可以看到,有了RocketMQ延迟消息之后,我们处理上就简单很多,只需要发消息,和接收消息就行了,系统之间完全解耦了。但是因为延迟消息的时长受到了限制,所以并不是很灵活。 如果我们的业务上,关单时长刚好和RocketMQ延迟消息支持的时长匹配的话,那么是可以基于RocketMQ延迟消息来实现的。否则,这种方式并不是最佳的。(但是在RocketMQ 5.0中新增了基于时间轮实现的定时消息,可以解决这个问题!) 七、RabbitMQ死信队列 延迟消息不仅在RocketMQ中支持,其实在RabbitMQ中也是可以实现的,只不过其底层是基于死信队列实现的。 当RabbitMQ中的一条正常的消息,因为过了存活时间(TTL过期)、队列长度超限、被消费者拒绝等原因无法被消费时,就会变成Dead Message,即死信。 当一个消息变成死信之后,他就能被重新发送到死信队列中(其实是交换机-exchange)。 那么基于这样的机制,就可以实现延迟消息了。那就是我们给一个消息设定TTL,然但是并不消费这个消息,等他过期,过期后就会进入到死信队列,然后我们再监听死信队列的消息消费就行了。 而且,RabbitMQ中的这个TTL是可以设置任意时长的,这就解决了RocketMQ的不灵活的问题。 但是,死信队列的实现方式存在一个问题,那就是可能造成队头阻塞,因为队列是先进先出的,而且每次只会判断队头的消息是否过期,那么,如果队头的消息时间很长,一直都不过期,那么就会阻塞整个队列,这时候即使排在他后面的消息过期了,那么也会被一直阻塞。 基于RabbitMQ的死信队列,可以实现延迟消息,非常灵活的实现定时关单,并且借助RabbitMQ的集群扩展性,可以实现高可用,以及处理大并发量。他的缺点第一是可能存在消息阻塞的问题,还有就是方案比较复杂,不仅要依赖RabbitMQ,而且还需要声明很多队列(exchange)出来,增加系统的复杂度 八、RabbitMQ插件 其实,基于RabbitMQ的话,可以不用死信队列也能实现延迟消息,那就是基于rabbitmq_delayed_message_exchange插件,这种方案能够解决通过死信队列实现延迟消息出现的消息阻塞问题。但是该插件从RabbitMQ的3.6.12开始支持的,所以对版本有要求。 这个插件是官方出的,可以放心使用,安装并启用这个插件之后,就可以创建x-delayed-message类型的队列了。 前面我们提到的基于私信队列的方式,是消息先会投递到一个正常队列,在TTL过期后进入死信队列。但是基于插件的这种方式,消息并不会立即进入队列,而是先把他们保存在一个基于Erlang开发的Mnesia数据库中,然后通过一个定时器去查询需要被投递的消息,再把他们投递到x-delayed-message队列中。 基于RabbitMQ插件的方式可以实现延迟消息,并且不存在消息阻塞的问题,但是因为是基于插件的,而这个插件支持的最大延长时间是(2^32)-1 毫秒,大约49天,超过这个时间就会被立即消费。但是他基于RabbitMQ实现,所以在可用性、性能方便都很不错 九、Redis过期监听 很多用过Redis的人都知道,Redis有一个过期监听的功能, 在 redis.conf 中,加入一条配置notify-keyspace-events Ex开启过期监听,然后再代码中实现一个KeyExpirationEventMessageListener,就可以监听key的过期消息了。 这样就可以在接收到过期消息的时候,进行订单的关单操作。 这个方案不建议大家使用,是因为Redis官网上明确的说过,Redis并不保证Key在过期的时候就能被立即删除,更不保证这个消息能被立即发出。所以,消息延迟是必然存在的,随着数据量越大延迟越长,延迟个几分钟都是常事儿。 而且,在Redis 5.0之前,这个消息是通过PUB/SUB模式发出的,他不会做持久化,至于你有没有接到,有没有消费成功,他不管。也就是说,如果发消息的时候,你的客户端挂了,之后再恢复的话,这个消息你就彻底丢失了。(在Redis 5.0之后,因为引入了Stream,是可以用来做延迟消息队列的。) 十、Redis的zset 虽然基于Redis过期监听的方案并不完美,但是并不是Redis实现关单功能就不完美了,还有其他的方案。 我们可以借助Redis中的有序集合——zset来实现这个功能。 zset是一个有序集合,每一个元素(member)都关联了一个 score,可以通过 score 排序来取集合中的值。 我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为 score 和 member。这样redis会对zset按照score延时时间进行排序。然后我们再开启redis扫描任务,获取”当前时间 > score”的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。 使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是在高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用加分布式锁解决,但是这样做也会降低吞吐型。 但是,在大多数业务场景下,如果幂等性做得好的,多个消费者取到同一个订单号也无妨。 十一、Redission + Redis 上面这种方案看上去还不错,但是需要我们自己基于zset这种数据结构编写代码,那么有没有什么更加友好的方式? 有的,那就是基于Redisson。 Redisson是一个在Redis的基础上实现的框架,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。 Redission中定义了分布式延迟队列RDelayedQueue,这是一种基于我们前面介绍过的zset结构实现的延时队列,它允许以指定的延迟时长将元素放到目标队列中。 其实就是在zset的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候,redission会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。 大致思路就是这样的,感兴趣的大家可以看一看RDelayedQueue的具体实现。 基于Redisson的实现方式,是可以解决基于zset方案中的并发重复问题的,而且还能实现方式也比较简单,稳定性、性能都比较高。 总结 我们介绍了11种实现订单定时关闭的方案,其中不同的方案各自都有优缺点,也各自适用于不同的场景中。那我们尝试着总结一下: 实现的复杂度上(包含用到的框架的依赖及部署): Redission > RabbitMQ插件 > RabbitMQ死信队列 > RocketMQ延迟消息 ≈ Redis的zset > Redis过期监听 ≈ kafka时间轮 > 定时任务 > Netty的时间轮 > JDK自带的DelayQueue > 被动关闭 方案的完整性: Redission ≈ RabbitMQ插件 > kafka时间轮 > Redis的zset ≈ RocketMQ延迟消息 ≈ RabbitMQ死信队列 > Redis过期监听 > 定时任务 > Netty的时间轮 > JDK自带的DelayQueue > 被动关闭 不同的场景中也适合不同的方案: 自己玩玩:被动关闭 单体应用,业务量不大:Netty的时间轮、JDK自带的DelayQueue、定时任务 分布式应用,业务量不大:Redis过期监听、RabbitMQ死信队列、Redis的zset、定时任务 分布式应用,业务量大、并发高:Redission、RabbitMQ插件、kafka时间轮、RocketMQ延迟消息 总体考虑的话,考虑到成本,方案完整性、以及方案的复杂度,还有用到的第三方框架的流行度来说,个人比较建议优先考虑Redission+Redis、RabbitMQ插件、Redis的zset、RocketMQ延迟消息等方案。
技术
# 分布式
酷游
1月22日
0
7
0
上一页
1
...
5
6
7
...
112
下一页
易航博客