木偶's Blog

如果发现能力无法支撑自己的野心,那就静下心来学习吧

1.1 Magnet

适用于 macOS,Windows 自带的分屏功能已经足够强大

非常简单粗暴好用!

image-20211222164957305

Magnet 除了拖拽和快捷键调整布局,没有更多功能,也就没有了自己设置的麻烦,胜在简单。不想自己配置的用户,看一眼快捷键表就能上手。

image-20211222165037679

但需要收费,可以在打折的时候购入。破解版在阿里云盘

2 US1 指纹加密闪存盘

品牌:Netac

制造商:深圳市朗科科技股份有限公司

技术支持/客服热线:400-830-3662

目前已知的可远程控制计算机的品牌有:

向日葵、花生壳、natapp.cn

向日葵远程控制

国内市场老大哥,个人免费版十分强大,满足个人日常需求。

  1. 白板模式

    在该模式下,可以在屏幕上进行涂写,被远程控制的电话上会同步显示除涂写的内容

  2. 兼容几乎所有系统

    兼容了市面上所有常见的系统,包括 Linux 和移动端都是可以使用的。

ToDesk

目前正在使用的远程控制软件,轻量,好用

不为人知的搜索技巧

一张照片查出你的拍摄地点

随着智能手机的普及,现在人们大部分使用手机进行拍照,而大多数相机已经默认开启地理位置。在开启了这种功能的情况下拍摄的照片会自动存有你所拍照地点的经纬度信息:

image-20210904125524453

有了经纬度信息,我们可以在 MagicExif 软件里查询到照片拍摄地的具体地址(精确到门牌号的那种地步!!!)

语音通话获取IP地址

我们在 QQ 聊天时都是通过数据进行传输,那么使用一个抓包工具,只要数据传输到对方并且对方在线,我们就可以获取到对方的 IP 信息:

image-20210904125621675

当我们打开这个工具的时候,只需要给对方拨打一个电话,不管对方有没有接听,你都可以获取到对方的 IP 地址:

通过IP进行定位

当我们获取到了对方的 IP 地址之后,也可以通过 IP 进行大概位置的定位(有五公里以内的误差):

IP 查询网址:www.ipplus360.com

image-20210904125722445

利用经纬度进行二次解析

当我们通过IP地址进行查询后可以获得对方的大概地理位置,如果想进行二次精确定位的话,我们可以复制经纬度,在解析网站里面进行查询:

经纬度解析网站:map.yanue.net

image-20210904125804786

通过邮箱/手机号查询你注册过的网站

查询网址:www.reg007.com

这个网站就是利用你提交的信息去进行模拟注册来查询是否在该平台已经注册过:

image-20210904125847384

利用支付宝查询你的名字

随意转账一笔大额资金给对方,这时,为了资金安全,支付宝会显示对方的名字,要求你补全对方的姓氏。

那么,我们可以使用常用的姓氏一个个进行尝试来暴力破解获取到对方全名。

image-20210904125928662

把知道的信息丢给百度查询

image-20210904130004300

这只是一个例子,不仅是QQ,还有 微信/常用ID/手机号 全部都可以进行查询,不法分子就可以利用这些信息进行电信诈骗!

防止被人肉的措施

  1. 不要在网络上留下自己的QQ,手机号码等相关个人信息,否则会被搜索引擎保存成快照,从而被不法分子所获取;
  2. 不要在所有网站使用同一个用户名;
  3. 最好使用两张手机卡,一张日常通话使用,另一张在网上注册信息时用于接收验证码等操作;
  4. QQ,微信等社交软件最好不要使用同一个账户。

1.1 场景

下面举例:

当通过组件调用 van-dialog 时,dialog 内容过多,对话框没有出现滚动样式,现通过以下的 css 语句解决该问题

1
2
3
4
.van-dialog__content {
max-height: 400px;
overflow: scroll;
}

结果发现,样式没有生效。

1.2 解决方案

采用以下方式,添加 /deep/,让其作用域往下钻

1
2
3
4
/deep/ .van-dialog__content {
max-height: 400px;
overflow: scroll;
}

样式生效,问题解决

1.3 拓展说明

在 Vue 中,关于样式有几个规范:

  • 一是样式写在文件的最后。
  • 二是使用 class,而不直接为标签名称写样式。
  • 三是要 scoped。

尤其是 scoped 很重要,因为 Vue 并不是我们传统的一个页面一个页面的文件,如果不 scoped,会发生样式干扰。

假如我们使用了一个 van-grid-item 组件,我们通过检查,发现生成 HTML 的结构是:

  • 外层 div 套了一个内层 div。
  • 外层 div 的 class 是:van-grid-item。
  • 内层 div 的 class 是:van-grid-item__content。

于是我们写样式:

1
2
3
4
5
6
7
8
9
<style lang="less" scoped>
.van-grid-item
border:1px solid red;
}
.van-grid-item .van-grid-item__content
background: #f00
color: #fff;
}
</style>

我们会发现第一个生效了,但是第二个没生效,这是因为 Vue 只认组件本身那层 class,其内部继续产生的 class 是不认的,怎么解决呢?加上 /deep/ 让其作用域往下钻,如下:

1
2
3
4
5
6
<style lang="less" scoped>
/deep/.van-grid-item .van-grid-item__content
background: #f00
color: #fff;
}
</style>

SIT 与 UAT 的区别

在企业级软件的测试过程中,经常会划分为三个阶段:单元测试、SIT 和 UAT。

如果开发人员足够,通常还会在 SIT 之前引入代码审查机制(Code Review)来保证软件符合客户需求且流程正确。

下面简单介绍一下 SIT 和 UAT 的基本情况。

SIT(System Integration Testing)系统集成测试,也叫做集成测试,是软件测试的一个术语,在其中单独的软件模块被合并和作为一个组测试。它在单元测试以后和在系统测试之前。集成测试在已经被单元测试检验后进行作为它的输入模式,组织它们在更大的集合,和递送,作为它的输出,集成系统为系统测试做准备。集成测试的目的是校验功能、性能和可靠性要求,配置在主设计项目中。

UAT(User Acceptance Testing)用户验收测试,通常是由最终软件的用户(通常这些用户不了解软件的具体逻辑,而对业务逻辑却相当熟悉)进行的测试,因此是面向最终用户的测试,结束之后通常就可以发布生产环境。

区别与联系

SIT 是集成测试,UAT 是验收测试

  • 从时间上看,UAT 要在 SIT 后面,UAT 测试要在系统测试完成后才开始。

  • 从测试人员看,SIT 由公司的测试员来测试,而 UAT 一般是由用户来测试。

它们两个之间的专注点是不一样的。UAT 主要是从用户层面这些去考虑和着手测试,而 SIT 主要是系统的各个模块的集成测试。这在整个软件过程理论的基础知识中相当重要的。理论上讲 SIT 是由专业的测试人员去完成,UAT 是由用户去做的。

如果按照规范来的话,做 UAT 测试的人一定是要对业务很精通的,并且是具有代表性的用户,关注的东西就是业务流程是否通畅是否符合业务的需要,主要以需求分析文档为重要参考,还有一些用户的操作习惯等等一系列的东西。

从0到网站部署_硬件准备

购买域名

网站上线运营就必须要购买域名,而万网是选择相对比较多的,万网目前在阿里旗下,首先你要有一个阿里云的账号,阿里云官方网站 https://www.aliyun.com/,然后进入管理控制台,然后购买域名。域名的价格不是很贵 ,一年也就几十元吧。

不过起域名是件很头疼的事情,域名要简单,好记,朗朗上口,最好和自己以后的网站昵称紧密相关。不过你会发现,你能想到的域名基本上都被注册过了,域名购买之后建议去实名认证。

ICP备案

Why

网站备案是国家相关部门要求的,在国内的所有网站都必须备案(使用海外服务器则不需要备案),备案之后,域名备案的主体信息及运营者将对域名提供的内容负法律责任的。

未备案的域名不能使用国内服务器;未备案的域名不能使用很多主流的推广手段,例如百度推广和微信推广,同时也会影响网站信誉度。

How

像阿里云,百度云,腾讯云都会提供域名备案的业务,但建议你使用哪里的服务器就在哪里备案,例如你使用的是阿里云,那你就在阿里云进行备案。因为有些云服务器提供商会要求只能接入自己平台上备案的域名。我记得我之前是在百度云备案的域名,但是在接入阿里云服务器无法接入,那就蛋疼了。

备案没有想象中那么复杂,但也不简单,填写各种信息,各个平台都有自己的备案流程和教程,自己可以看看,大体相同。整个备案流程下来基本上 要20天左右。因此,只要你想运营个人网站,第一步就是准备域名和备案,省的耽误你网站上线。

服务器

要把网站代码部署到服务器中才能让别人访问。

关于服务器的选择

  1. 云虚拟主机:价格便宜,虚拟主机只能使用其预装好的web server和数据库

    例如,阿里云虚拟主机 Window版本下是IIS和SQL Server,Linux版本下是Apache和MySQL,这两种都不支持Java Web应用。通过可视化界面控制,用户不需要自己搭建网站的运行环境和数据库等,只需用户上传代码即可。适合访问量较低的个人网站使用。具体的使用方法可百度关键词“云虚拟部署网站”

  2. 云服务:价格较贵,相当于用户远程控制的一台电脑,用户可根据需要搭建不同的应用环境,

    例如用户可在云服务器上搭建javaweb php asp.net等多种网站部署环境,在购买云服务器时,可以购买镜像(即网站或应用程序的开发环境)勿需用户自己搭建,支持一键部署,用户也可以选择公共镜像,自己搭建网站的运行环境和数据库等。

  3. 类似百度 BAE 的PaaS平台,此平台可集成多种应用的运行环境,例如下图

    image-20210904131539628

    只能选择一种集成环境环境,一旦选择不能改变,此平台的好处是用户不用关心开发环境,只要将代码上传发布即可。缺点就是,使用了此平台还要购买云数据库,因此最后,购买平台+购买云数据库的价格相当于使用云服务器的价钱。

域名解析

域名解析的意思,就是把申请的已备案成功后的域名指向部署网站服务器的ip地址或二级域名,解析成功后,别人就可以通过域名访问你的网站了。

首先,在哪里购买的域名,就在哪设置域名解析,假如你的域名是在阿里万网购买的,登陆阿里云的控制台后,进入域名管理界面,如下图

image-20210904131653752

点击域名后面的‘解析’后,如下图

image-20210904131709884

点击‘添加解析’,如下图,这里你要注意以下两点:

  1. 记录类型的选择

  2. 记录值

一般记录类型选择 A 或者 CNAME。

  • 当记录类型选择 A 时,那么记录值就填写部署网站服务器的 ip 地址

  • 当记录类型选择 CNAME 时,那么记录值就填写部署网站服务器的二级域名

一般如果你部署网站的环境选择的是类似百度 BAE 的 PaaS 平台,那么平台会提供一个二级域名,如果选择的是云虚拟机或云服务器则会提供一个 ip 地址。

image-20210904131852710

从 0 到网站部署 _ 软件准备

技术选型

以下推荐适合有点编程基础,或者想快速搭建个人网站的,如果你是想锻炼自己的开发能力,那你就自己从 0 开始 codeing。

我个人非常推荐基于 PHP 开源的 CMS 系统 WordPress,虽然不少人鄙视 php,但不得不承认 php 在个人网站和中小企业网站上还是很值得选择的。

WordPress 基本上满足个人网站的功能,用户注册,登陆,内容发布,管理,评论等,而且扩展性强,主题模板和功能插件非常丰富,即使没有 php 编程的基础也可以搭建一套完美的网站系统,如果有 php 开发技术也可以根据需要进行二次开发。

这里就不多介绍了,有需要的可以自己百度。当然 Python,Java 等也都有类似 WordPress 开源的系统,具体的大家可以百度关键词,例如‘Python 开源 cms 系统’。

SEO 搜索

  1. 网站上线后,要主动向各个搜索引擎提交自己的网站入口,http://www.baidusourl.com/,这样别人就可以通过网站名称搜索到你的网站。
  2. 生成自己的网站页面地图。有利于搜索引擎收录网站内容,以便用户使用关键词搜索到你的网站内容。如果你使用的是 WordPress,那极力推荐你使用 Baidu Sitemap Generator 这个插件,可以自动生成一个静态的站点地图页面,可以手动提交给搜索引擎加快百度的收录速度。

以上只是最入门也是必备的 SEO 搜索优化,更多的优化,大家可以百度搜索关键词“网站 SEO 搜索优化”

网站安全

必备安全功能

  1. 记录注册用户的 IP 地址。这个简单,一个字段存数据库就好了。
  2. 防止访客 F5 频繁刷新页面。网站使用的技术不同,实现方式也不相同,大致原理就是,根据访问用户的 ip,用 session 记录 1 秒内访问的次数,如果访问的过多就输出拒绝访问的提示。如果这个功能没有,长按 F5 就可以把你的网站服务器刷爆。
  3. 黑名单功能。就是来防止用户恶意评论等不友好的操作或直接防止用户访问(一般这个可以通过服务器配置文件,配置禁止访问的 IP 地址)。基本上都是在用户使用某种网站功能时 先判断用户 IP 是否在黑名单中,如果在,禁止使用,否则就放行。如果没有这种功能,遇到了变态用户会恶心到你吐。如果你使用的是 WordPress,这里我也推荐你使用 WP-Ban 这个插件。

接入第三方的云加速平台

当然这个平台的高级功能是收费的,但个人网站使用其免费功能也就够用了,例如 360 云加速百度云加速抗 D 保等平台。

这里我首推 360 云加速,毕竟是做安全出身;个人认为使用加速平台的免费功能最大的好处就是可以隐藏网站真实的 IP 地址,其次是支持配置 IP 黑名单(这个功能有些平台是收费的),也有静态文件缓存,加速,图片防盗链等功能。

用户统计

  1. 根据自己需要开发用户统计的功能。这个不多讲,根据选择的技术方案实现也各不相同。
  2. 接入第三方的统计平台。个人使用过百度统计CNZZ 数据专家。推荐使用后者,功能很强大。日访问量,独立 IP,访客 ip….等等

最后

以上只是经验之谈,都是些最基本,但也是必备的功能。

1.1 操作系统

  • 深⼊入理理解计算机系统
  • 计算机是怎么跑起来的
  • 程序是怎么跑起来的

1.2 网络协议

TCP/IP 协议详解卷一: 协议

http 权威指南

图解 TCP/IP

1.3 Linux

  • 鸟哥的 Linux 私房菜
  • Linux 命令行与 shell 脚本编程⼤大全
  • Linux 内核设计的艺术 (关于内核,难度较大)

1.4 数据库

  • 深入浅出 MySQL 数据库
  • MySQL 技术内幕:InnoDB 存储引擎
  • Redis 开发与运维

1.5 编程语言

  • python 核心编程 (Python)
  • C Primer Plus(C 语言)

1.6 云计算&虚拟化

  • Docker 技术入门与实战
  • Kubernetes 权威指南

1.7 网站架构

  • 大型网站技术架构 核心原理理与案例例分析

如何实现延迟任务

场景描述

假设现在有一个需求:生成订单 30 分钟未支付,则自动取消,如何实现?又或者,生成订单 60秒 后,给用户发短信。

像上面这样子的需求,就属于延时任务。

这个延时任务和定时任务的区别究竟在哪里呢?

  1. 定时任务有明确的触发时间,延时任务没有
  2. 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期
  3. 定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

解决方案总结

解决方案有很多种,但各有优缺点,目前来看,采用 Redis 是最优的解法。

分别如下:

  • 数据库轮询
  • JDK的延迟队列

数据库轮询

思路

该方案通常是在小型项目中使用,通过一个线程定时去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作

实现

用 quartz 来实现的

依赖引入

1
2
3
4
5
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
System.out.println("要去数据库扫描啦。。。");
}

public static void main(String[] args) throws Exception {
// 创建任务
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1").build();
// 创建触发器 每 3秒 钟执行一次
Trigger trigger = TriggerBuilder
.newTrigger()
.withIdentity("trigger1", "group3")
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(3).repeatForever())
.build();
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
// 将任务及其触发器放入调度器
scheduler.scheduleJob(jobDetail, trigger);
// 调度器开始调度任务
scheduler.start();
}
}

优缺点

优点:

简单易行,支持集群操作

缺点:

  1. 对服务器内存消耗大
  2. 存在延迟,比如你每隔 3分钟 扫描一次,那最坏的延迟时间就是 3分钟
  3. 假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大

JDK的延迟队列

思路

该方案是利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。

DelayedQueue 实现工作流程如下图所示

image-20210830091336105
  • Poll():获取并移除队列的超时元素,没有则返回空
  • take():获取并移除队列的超时元素,如果没有,则 wait 当前线程,直到有元素满足超时条件,返回结果。

实现

定义一个类 OrderDelay 实现 Delayed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OrderDelay implements Delayed {
private String orderId;
private long timeout;

OrderDelay(String orderId, long timeout) {
this.orderId = orderId;
this.timeout = timeout + System.nanoTime();
}

public int compareTo(Delayed other) {
if (other == this)
return 0;
OrderDelay t = (OrderDelay) other;
long d = (getDelay(TimeUnit.NANOSECONDS) - t
.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}

// 返回距离你自定义的超时时间还有多少
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(),TimeUnit.NANOSECONDS);
}

void print() {
System.out.println(orderId+"编号的订单要删除啦。。。。");
}
}

延迟队列,使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DelayQueueDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
List<String> list = new ArrayList<String>();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
DelayQueue<OrderDelay> queue = newDelayQueue<OrderDelay>();
long start = System.currentTimeMillis();
for(int i = 0; i < 5; i++){
// 延迟三秒取出
queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));
try {
queue.take().print();
System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

demo 运行结果:

image-20210830091925538

可以看到都是延迟 3秒,订单被删除

优缺点

优点

效率高,任务触发时间延迟低。

缺点

  1. 服务器重启后,数据全部消失,怕宕机
  2. 集群扩展相当麻烦
  3. 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM异常
  4. 代码复杂度较高

时间轮算法

思路

image-20210830092103301

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每次跳动称为一个 tick。定时轮有 3个 重要的属性参数:

  • ticksPerWheel(一轮的 tick 数),
  • tickDuration(一个 tick 的持续时间)
  • timeUnit(时间单位)

当 ticksPerWheel=60,tickDuration=1,timeUnit=秒,就和现实中的秒针走动完全类似了

如果当前指针指在 1 上面,有一个任务需要 4秒 以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在20秒之后执行怎么办?这个环形结构槽数只到 8,如果要 20秒,指针需要多转2圈。位置是在 2圈之后 的 5 上面(20 % 8 + 1 = 2...5,即 2 余 5)

实现

用 Nett y的 HashedWheelTimer 来实现

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask{
boolean flag;
public MyTimerTask(boolean flag){
this.flag = flag;
}
public void run(Timeout timeout) throws Exception {
// TODO Auto-generated method stub
System.out.println("要去数据库删除订单了。。。。");
this.flag =false;
}
}
public static void main(String[] argv) {
MyTimerTask timerTask = new MyTimerTask(true);
Timer timer = new HashedWheelTimer();
timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
int i = 1;
while(timerTask.flag){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(i + "秒过去了");
i++;
}
}
}

优缺点

优点

  • 效率高
  • 任务触发时间延迟时间比 delayQueue 低
  • 代码复杂度比 delayQueue 低。

缺点

  • 服务器重启后,数据全部消失,怕宕机
  • 集群扩展相当麻烦
  • 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM异常

Redis缓存

Redis 实现延时任务会有两种思路

思路一:zset

利用 redis 的 zset。zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值。

将订单超时时间戳与订单号分别设置为 score 和 member,系统扫描第一个元素判断是否超时,具体如下图所示:

image-20210830094356834

实现一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class AppTest {
private static final String ADDR = "127.0.0.1";
private static final int PORT = 6379;
private static JedisPool jedisPool = new JedisPool(ADDR, PORT);

public static Jedis getJedis() {
return jedisPool.getResource();
}

// 生产者,生成 5个 订单放进去
public void productionDelayMessage() {
for(int i = 0; i < 5; i++) {
//延迟3秒
Calendar cal1 = Calendar.getInstance();
cal1.add(Calendar.SECOND, 3);
int second3later = (int) (cal1.getTimeInMillis() / 1000);
AppTest.getJedis().zadd("OrderId", second3later, "OID0000001" + i);
System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID0000001" + i);
}
}

// 消费者,取订单
public void consumerDelayMessage() {
Jedis jedis = AppTest.getJedis();
while(true){
Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 1);
if(items == null || items.isEmpty()) {
System.out.println("当前没有等待的任务");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
continue;
}
int score = (int) ((Tuple)items.toArray()[0]).getScore();
Calendar cal = Calendar.getInstance();
int nowSecond = (int) (cal.getTimeInMillis() / 1000);
if(nowSecond >= score){
String orderId = ((Tuple)items.toArray()[0]).getElement();
jedis.zrem("OrderId", orderId);
System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId);
}
}
}

public static void main(String[] args) {
AppTest appTest = new AppTest();
appTest.productionDelayMessage();
appTest.consumerDelayMessage();
}

}

image-20210830094535676

可以看到,几乎都是3秒之后,消费订单。

然而,这一版存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号,我们上测试代码 ThreadTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadTest {
private static final int threadNum = 10;
private static CountDownLatch cdl = newCountDownLatch(threadNum);
static class DelayMessage implements Runnable {
public void run() {
try {
cdl.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
AppTest appTest =new AppTest();
appTest.consumerDelayMessage();
}
}
public static void main(String[] args) {
AppTest appTest =new AppTest();
appTest.productionDelayMessage();
for(int i = 0; i < threadNum; i++) {
new Thread(new DelayMessage()).start();
cdl.countDown();
}
}
}

image-20210830094659763

显然,出现了多个线程消费同一个资源的情况。

解决方案:

  1. 用分布式锁,但是用分布式锁,性能下降了,该方案不细说。
  2. 对 ZREM 的返回值进行判断,只有大于 0 的时候,才消费数据

于是将 consumerDelayMessage() 方法里的

1
2
3
4
5
if(nowSecond >= score) {
String orderId = ((Tuple)items.toArray()[0]).getElement();
jedis.zrem("OrderId", orderId);
System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单 OrderId 为" + orderId);
}

修改为:

1
2
3
4
5
6
7
if(nowSecond >= score) {
String orderId = ((Tuple)items.toArray()[0]).getElement();
Long num = jedis.zrem("OrderId", orderId);
if( num != null && num > 0){
System.out.println(System.currentTimeMillis()+ "ms: redis 消费了一个任务:消费的订单 OrderId 为" + orderId);
}
}

在这种修改后,重新运行 ThreadTest 类,发现输出正常了

思路二:键空间机制

利用 Redis 的 Keyspace Notifications,即键空间机制,就是利用该机制可以在 key 失效之后,提供一个回调,实际上是 Redis 会给客户端发送一个消息。需要 Redis 版本 2.8 以上。

实现二

配置 redis.conf

1
notify-keyspace-events Ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RedisTest {
private static final String ADDR = "127.0.0.1";
private static final int PORT = 6379;
private static JedisPool jedis = new JedisPool(ADDR, PORT);
private static RedisSub sub = new RedisSub();

public static void init() {
new Thread(new Runnable() {
public void run() {
jedis.getResource().subscribe(sub, "__keyevent@0__:expired");
}
}).start();
}

public static void main(String[] args) throws InterruptedException {
init();
for(int i = 0; i < 10; i++) {
String orderId = "OID000000"+i;
jedis.getResource().setex(orderId, 3, orderId);
System.out.println(System.currentTimeMillis() + "ms: " + orderId + "订单生成");
}
}

static class RedisSub extends JedisPubSub {
<ahref='http://www.jobbole.com/members/wx610506454'>@Override</a>
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + "ms: " + message + "订单取消");
}
}
}

结果如下:

image-20210830093745571

优缺点

优点

  • 使用 Redis 作为消息通道,消息都存储在 Redis 中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。
  • 做集群扩展相当方便
  • 时间准确度高

缺点:方案二

redis 的 pub/sub 机制存在一个硬伤,官网内容如下:

Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost.

因此,方案二不是太推荐。当然,如果对可靠性要求不高,可以使用。

消息队列

可以采用 rabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列:

  • RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter
  • RabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 dead letter,则按照这两个参数重新路由。

结合以上两个特性,就可以模拟出延迟消息的功能

优缺点

优点

  • 高效
  • 可以利用 rabbitMQ 的分布式特性轻易的进行横向扩展
  • 消息支持持久化增加了可靠性。

缺点

本身的易用度要依赖于 rabbitMQ 的运维。因为要引用 rabbitMQ,所以复杂度和成本变高

0%