2023年3月

分布式事件总线在分布式开发(或微服务开发)时,是极为重要的架构手段。它可以分解响应时长,可以削峰,可以做最终一致性的分布式事务,可以做业务水平扩展。

1、分解响应时长

比如我们的一个接口处理分为四段代码,别分耗时:A段(0.5s),B段(1s),C段(0.5s),D段(3s)。如果同步响应的话,用户一共需要等待 5s,这个体验肯定不怎么好了。我们可以借分布式事件总线,做完A后,发一个事件,由事件订阅者再去完成B,C,D;那用户的感觉就是0.5S就完成了,体验就会比较好。(如果是单体,可以自己订阅;如果是分布式,可以由其它服务订阅)

2、 削峰

这个事情跟“响应时长”有极大的关系。比如一个接口响应需要5s,每秒请求数有200个,那舜间的并行请求就会有1000个(上一秒的未处理完,下一秒的又来了嘛),这个请求就会堆积如山,山峰也会越来越高。突然一波大流量就服务器可能挂了。

如果是0.5s,那并行处理就只会有100个。当前服务器的内存和cpu消耗也会10倍级的下降。

3、 做最终一致性的分布式事务

事件一但发送成功,中间件就会一直“盯”着你把事件消费成功为止。如果消费失败了,它会过段时间再发给你,直到你成功为止。(处理时,要注意“幂等性”控制。分布式环境,总会有不确定原因)

4、 业务水平扩展

这个是“分布式事件总线”的灵魂级妙处。你开发了一个用户注册的接口。一周后,产品说“用户注册完送5个Q币”,旧的生产环境不用动,你只需要开发一个新的服务,订阅注册完成事件做处理;一个月后,产品说“用户注册完成后,给他推送电信的大礼包活动”;后来产品又说“用户注册后7天后,如果有上线3次,再送10个Q币”。。。这个就是指“业务水平扩展”了。在不动原代码和原服务,就扩展业务。

如果我们还有一个FaaS平台,可以动态的扩展事务。产品爱怎么搞,就怎么搞。像
Water
就有这样的动态事件功能(在线编即,实时生效或删除)。

多年前的Leftpad 撤包事件使得React 、 Babel 和许多流行的npm模块都受到波及,无法正常运行。

这些受到影响的模块都引入了一个叫做 left-pad 的模块。
以下就是这十一行代码:

module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}

而其中的原因大概是这样:作者 Azer 写了一个叫
kik
的工具和某个公司同名了,这天公司的律师要求其删掉这个模块,把 kik 这个名字“让”给他们,作者不答应,律师就直接找 NPM 了,而 NPM 未经作者同意就把包的权限转移给了这家公司。于是,Azer 一怒冲冠,将他所有的 NPM 包全部删掉了。

有意思的是,社区中许多的模块都选择引入这个十一行的模块,而不是花上两分钟的时间自己去实现这个简单的字符串填充功能。

这不是npm包管理第一次出问题,也不会是最后一次。

Leftpad撤包事件、event-stream投毒事件、Ant Design彩蛋时间,使得我们不得不开始重新思考npm生态真的存在的问题,甚至去问自己:
我们是不是早已忘记该如何好好地编程?

  • NPM模块粒度
  • 代码风格
  • 代码质量/效率
  • 过度依赖

这种过度依赖其他npm模块的做法是不是解决问题的正确方式呢?现在,一个空白项目模板一装好就要引入两万八千多个文件、依赖成百上千个其他的npm模块。这太疯狂了、而且过度复杂。

那么我们可以做些什么?把命运掌握在自己手里

  • 在发布前“冻结”依赖模块的版本号。这让我们对安装的依赖有信心,依赖模块的版本都是我们验证、测试过的。
  • 在发布前“打包”依赖模块到自己项目。这让我们可以坦然面对我们依赖的某个模块“没有了”这样的囧境。

冻结依赖模块:

冻结依赖模块的版本号最简单的办法就是直接在 package.json 里面写死版本号,但是这解决不了深度依赖的问题。我们来看个例子。 假设有下面这样的依赖:

A@0.1.0 
└─┬ B@0.0.1  
  └── C@0.0.1

A 模块依赖了 B 模块,B 模块又依赖了 C 模块。我们可以将 B 模块的依赖写死成 0.0.1 版本,但是如果 B 模块对 C 模块的依赖写的是
C@0.0.1
,会怎样?

这时候 C 模块更新到了 0.0.2 版本,虽然我们安装的 B 模块是
B@0.0.1
,但是安装的 C 模块却是
C@0.0.2
。如果不巧这个
C@0.0.2
刚好有 bug,那我们的模块很有可能就不能正常工作了。 实际上,NPM 提供了一个叫做
npm shrinkwrap
的命令来解决这个问题:

NAME
  npm-shrinkwrap -- Lock down dependency versions

SYNOPSIS
  npm shrinkwrap

DESCRIPTION
  This  command  locks down the versions of a package's dependencies so that you can control exactly which versions of each  dependency  will be used when your package is installed.

这条命令会根据目前我们
node_modules
目录下的模块来生成一份“冻结”住的模块依赖(npm-shrinkwrap.json)。

还是上面的例子,我们在模块 A 的根目录执行
npm shrinkwrap
后,生成的
npm-shrinkwrap.json
文件内容大概是下面这样:

{
    "name": "A",
    "dependencies": {
        "B": {
            "version": "0.0.1",
            "resolved": "http://registry.npmjs.com/B-0.0.1.tgz",
            "dependencies": {
                "C": {  
                 "version": "0.0.1",
                 "resolved": "http://registry.npmjs.com/C-0.0.1.tgz"
          }
            }
        }
    }
}

然后,当我们执行
npm install
时,依赖查找的“来源”不再是
package.json
,而是我们生成的
npm-shrinkwrap.json
,再也不会突然装上什么
C@0.0.2
了,依赖里面的模块版本都是我们验证、测试后的版本,让人安心。

注:
npm shrinkwrap
默认只会生成
dependencies
的依赖,不会生成
devDependencies
的依赖,如果你真的需要,可以加
--dev
参数。

打包依赖模块:

我们解决了依赖模块版本号的问题,但是每次安装时其实还是会去 NPM 的 registry 获取模块的 tgz 包然后进行安装。我们需要将这些依赖都打包进我们的项目。这可能会带来一些问题(比如:项目体积的增大),但是好处也是显而易见的。

上面生成的
npm-shrinkwrap.json
里面有个
resolved
字段,表示模块所在的位置,实际上这个字段完全可以写一个文件路径。所以,我们可以递归的遍历
npm-shrinkwrap.json
文件,将所有的 tgz 包先下载到我们项目的某个目录,然后改写
resolved
字段为对应的文件路径。这样的功能有开发者已经实现了,我们可以直接享用:
https://github.com/JamieMason/shrinkpack

于是,我们以后再进行
npm install --loglevel=http
时会发现依赖模块的安装根本没有网络请求了(因为依赖都在我们自己的仓库里了嘛)。

可能有人会说,为啥不直接把
node_modules
目录提交进仓库算了?原因主要是这样:

  • 有些模块需要编译,编译是和环境有关的,你当前的环境编译可用,其他环境直接使用该模块不一定能用。
  • node_modules
    目录里面啥东西都有,太凌乱,很容易把提交给搅乱。diff 时突然 diff 出
    node_modules
    下的源代码、README,你应该不想这样吧?

只存储模块的 tgz 包,安装编译的过程交给 NPM 命令更明智。

新方式

于是,现在我们使用 NPM 模块的正确姿势应该是这样了:

  1. 本地安装、更新需要的模块,测试、验证
  2. 执行
    npm shrinkwrap
    将依赖模块的版本冻结
  3. 执行
    shrinkpack .
    将依赖模块打包进仓库
  4. 提交代码(注意要将
    npm-shrinkwrap.json

    node_shrinkpack
    一起提交哦)
  5. 发布模块或者部署应用

如果你觉得这样很繁琐,可以定义一个 NPM 命令:

"scripts": {
  "pack": "npm shrinkwrap & shrinkpack ."
}

apt-get update更新时出现错误,提示Release文件已经过期,无论是使用kali官方源还是阿里源、中科大源都报该错误。

网上查找相关资料,签名出错需要下载数字签名,方案如下:

wget archive.kali.org/archive-key.asc //下载签名

apt-key add archive-key.asc //安装签名

此时又提示,找不到有效的OpenPGP数据,继续查找资料,方案如下:

wget https://download.docker.com/linux/ubuntu/gpg

sudo apt-key add gpg

pgp安装成功后,签名安装依然提示找不到有效的openpgp数据。

找了好久 终于知道了出现这个问题的原因:kali源升级了https解析,需要将kali源的http替换成https,如下:

然后apt-get update不再报数字签名错误。

「和我一起学 XXX」是我 2023 年的一个新企划,目的是向读者(也包括未来的自己)介绍我
正在学习
的某项新技术。文章会通过长期反复迭代的方式保持其内容的新鲜度。文章有较大内容更新时,会在文章开头进行更新时间说明(由于时间精力有限,更新的内容只能保障少数几个平台的同步,请见谅)。

1. 什么是 Three.js

Three.js 是一个
基于 WebGL

3D JavaScript 开源库
(遵循 MIT 协议),它使 JavaScript 开发者能够
更方便地
在 Web 应用中创建
3D 场景

请注意该定义的如下部分:

  • 基于 WebGL
    :WebGL 是一种 3D 绘图协议,对于开发者而言,它是一组更底层的绘图 API,它负责绘制点,线与三角形,使用 WebGL 绘制复杂的 3D 场景,需要非常多的代码;
  • 3D JavaScript 开源库
    :Three.js 基于非常宽松的 MIT 协议,这意味着您可以自由使用,修改 Three.js 代码创建商业应用;
  • 更方便地
    :就像 jQuery 基于 JavaScript 提供了更友好地 API 使开发者能够轻松地操作 DOM 一样,Three.js 也封装出更友好地 API 供开发者绘制 3D 场景,相较于使用 WebGL,使用 Three.js 绘制 3D 场景需要的代码量要少的多得多。
  • 3D 场景
    :它包含:
    • 3D 游戏;
    • 建筑设计和数据可视化看板;
    • AR,VR;
    • 虚拟展厅,虚拟商品展示;
    • 交互式展览,培训等;

您可以在 Three.js
官网
发现丰富的案例,它们从不同方面展示了 Three.js 的魅力和强大!

2. (我)为什么要学习 Three.js?

在了解 Three.js 是什么后,若选择继续学习,想必您有自己的理由。对于我而言,学习 Three.js 的主要动机是「
好玩
」(Just for fun!)。

我觉得能够在显示器上渲染 3D 场景是件很酷的事情,特别是它还可以通过 VR 头显设备让人们身临其境体验到一个由我创造的虚拟世界!

无论您学习的动机是否与我相同,都欢迎您和我一起持续探索 Web 3D 世界。我有信心带您一起踏入 3D 世界的大门!

3. 需要学习哪些内容?

在 Three.js 的
官方文档
中,您可以看到一个简单的 Three.js 应用包含了哪些模块:

threejs-structure.svg

您可以看到,图中包含了「
渲染器
(Renderer)」,「
场景
(Scene)」,「
摄影机
(Camera)」,「
网状物
(Mesh)」,「
3D 对象
(Object3D)」,「
灯光
(Light)」,「
几何体
(Geometry)」,「
材质
(Material)」和「
纹理
(Texture)」等元素,如果您从未接触过 Web 3D 世界,您可能有点摸不着头脑。

但是别担心,通过阅读本系列文章,您将能够掌握绝对大多数内容,并了解它们之间的关系。在往后的篇幅中,我将一一介绍这些名词并介绍它们对于构建 Web 3D 场景的意义所在,以及您应该如何正确地使用它们。通过完全掌握这些概念,您应该能够自己实现任意简单的 3D 场景。

我发现很多文章专注于介绍某种 3D 场景具体如何搭建,在本系列文章中,我不会这么做,我倾向于采用一种「自顶向下」的方法,让您理解到 Web 3D 世界的每个构成要素,然后您便能通过自由组合这些要素,搭建任意您感兴趣的 3D 场景。

因此接下来,本系列文章将会分为如下几个部分向您介绍 Three.js 技术的基本元素:

⚠️ 文章具体内容可能会根据实际情况有所增减。

3.1 搭建 Web 3D 场景

本章将介绍搭建 Web 3D 场景的必备要素(包含场景,物体与动画)和基本原理,通过本章的学习,您应该有能力开发出一个基本的 3D 场景,它类似于 Web 3D 世界的
Hello World
,标志您正式踏入 Web 3D 世界。

3.2 掌握几何体

本章将介绍 Three.js 提供的多个几何体元素以及它们的特性。它们将是未来您构建的各类 3D 场景中的主角。

3.3 掌握摄影机

摄影机的种类和位置不仅决定了我们观察 3D 世界的方式,也决定了物体的光影和色彩该如何被 GPU 渲染,本章我们将了解 Three.js 提供的摄影机种类以及如何操作它们。

3.4 掌握纹理

本章将介绍「纹理」这个概念,您可以将他理解为「贴图」,通过恰当地使用纹理,您可以让您的几何体成为具象的现实物体。

3.5 掌握材质

本章将介绍「材质」,即讨论物体的每个可见像素应该被如何着色的问题,通过掌握物体材质的设置方法,您的 3D 物体将会配合光影产生更加逼真的效果。

一、轮播图加缓存

有些知名网站首页被访问的频率很高,假设瞬间 1w个人在访问,首页的轮播图接口会执行1w次,1w次查询轮播图标的sql在执行,轮播图基本不变,首先我们给自己写的轮播图接口加缓存,我们可以用缓存数据库Redis来实现加缓存的需求

首先罗列一下文字版的逻辑,之后在代码上实现

  1. 当轮播图接口来了请求
  2. 先去缓存看看,如果有缓存,直接返回
  3. 如果没有缓存,则去数据库查询
  4. 然后拿到数据放到Redis中,缓存起来

通过代码实现加缓存

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from luffy_api.utils.common_response import APIResponse
from .models import Banner
from .serializer import BannerSerializer
from django.core.cache import cache


class BannerView(GenericViewSet, ListModelMixin):
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = BannerSerializer
    """没加缓存的逻辑"""
    # def list(self, request, *args, **kwargs):
    #     res = super().list(request, *args, **kwargs)
    #     return APIResponse(data=res.data, headers={'Access-control-Allow-Origin': '*'})
    """加缓存之后的逻辑"""
    def list(self, request, *args, **kwargs):
        # 查看缓存有没有数据
        banner_list = cache.get('banner_list')
        if banner_list:
            print('走了缓存')
            return APIResponse(data=banner_list)
        else:
            print('走了数据库')
            res = super().list(request, *args, **kwargs)
            cache.set('banner_list', res.data)
            return APIResponse(data=res.data)

image
Redis数据库也查到了缓存数据的信息
image

二、那么到底什么是双写一致性?

双写一致性指的是当我们更新了数据库的数据之后redis中的数据 也要同步去更新。使用redis读取数据的流程,当用户访问数据的时候,会先从缓存中读取数据,如果命中缓存的话,那么直接把缓存中的数据返回给用户,如果缓存中没有数据的话,先查询数据库把查询到的数据保存到缓存中,然后返回给用户。总结下来就是写入mysql,redis没动,数据不一致存在问题
image

三、如何解决双写一致性问题

  1. 修改数据,删除缓存
  2. 修改数据,更新缓存
  3. 定时更新,用celery

四、使用celery来解决双写一致性问题

通过截图解释代码的流程
image
在celery.py文件里面写如下代码

from datetime import timedelta
from celery import Celery
import os

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
# 提交的异步任务,放在里面
broker = 'redis://127.0.0.1:6379/1'
# 执行完的结果,放在这里
backend = 'redis://127.0.0.1:6379/2'
# 不要忘了include
app = Celery('test', broker=broker, backend=backend,
             include=['celery_task.banner_update_task'])

# 任务的定时配置(celery的配置文件)
app.conf.timezone = 'Asia/Shanghai'  # 时区
app.conf.enable_utc = False  # 是否使用UTC
# 定时任务
app.conf.beat_schedule = {
    'update_banner': {
        'task': 'celery_task.banner_update_task.update_banner',
        'schedule': timedelta(seconds=3),  # 时间对象
    },
}

在任务文件里面写如下代码

from .celery import app
from home.models import Banner
from home.serializer import BannerSerializer
from django.core.cache import cache
from django.conf import settings


@app.task
def update_banner():
    # 只要这个任务一执行,就更新轮播图的缓存
    banners = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    ser = BannerSerializer(instance=banners, many=True)
    for item in ser.data:
        item['image'] = settings.BACKEND_URL + item['image']

    cache.set('banner_list', ser.data)  # 会出问题,轮播图地址显示不全
    return True

启动worker

celery  -A celery_task  worker -l info -P eventlet

启动beat

celery -A celery_task beat -l info

确保过程中不出错,把前后端都重启一遍