‘编程学习’ 分类下的所有文章
2024七月23

BudIot 开源物联网设备平台v1.0发布

BUDIOT 是一个开源的、企业级的物联网平台,它集成了设备管理、协议解析、消息订阅、场景联动等一系列物联网核心能力,支持以平台适配设备的方式连接海量设备,支持在线下发指令实现远程控制,支持扩展水电气等各类计费业务场景。

本平台是在千万级设备实时计费物联网平台经验基础上,在不损失性能的前提下进行功能删减、结构优化而来,小而美,同时又具备灵活的扩展性。

源码: https://github.com/budwk/budiot

在线演示地址: https://demo.budiot.com 用户名: superadmin 密码: 1

官网: https://budiot.com

开发框架

基于自研 Java 微服务框架 https://budwk.com

简单说明

Jar 运行模块

  • budiot-access/budiot-access-gateway 设备网关,用于设备协议和 network 组件
  • budiot-access/budiot-access-processor 设备数据上报业务处理模块
  • budiot-server WEB 服务 API ,定时任务等

其他模块说明

  • budiot-access/budiot-access-network 网络组件,支持 TCP/MQTT/UDP/HTTP 等
  • budiot-access/budiot-access-protocol 设备协议开发包,内含 demo 示例
  • budiot-access/budiot-access-storage 设备数据存储,可扩展时序数据库等

前端模块

  • budiot-vue-admin Vue3 + Element-Plus

开发环境

  • OpenJDK 11
  • Redis 6.x
  • MariaDB 10.x
  • MongoDB 7.0.x
  • RocketMQ 5.2.x

设备上报有效数据存储

默认采用 MongoDB 7 的时序集合,可根据项目规模需要,扩展为 TDEngine 等时序数据库

2024七月9

Java项目运行一段时间后报错 Comparison method violates its general contract

java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Comparison method violates its general contract!

错误代码:
children.sort((o1, o2) -> (int) (o2.getLong("createdAt", 0) - o1.getLong("createdAt", 0)));

修复后:
children.sort((o1, o2) -> Long.compare(o2.getLong("createdAt", 0), o1.getLong("createdAt", 0)));

排序器 children.sort((o1, o2) -> (int) (o2.getLong("createdAt", 0) - o1.getLong("createdAt", 0))); 存在一个潜在的问题,即当时间戳的差值超出 int 的范围时,强制转换为 int 会导致数据溢出,从而导致比较结果不正确。

一个解决方法是使用 Long.compare 方法进行比较,而不是手动进行减法运算。Long.compare 方法确保了比较的对称性和一致性。

2024七月8

600万设备连接平台遇到的坑

MongoDB 的坑

listCollections

  • 问题:代码中存在 listCollections 操作,大量数据上报时,造成MongoDB CPU高升,使处理性能受到影响。经过检查,发现通信报文日表是自动创建的,每次都会判断集合是否存在;而mongo驱动包里判断集合是否存在的操作,就是先执行 listCollections;
  • 优化:提前创建好需要的集合,不要在数据上报的时候进行判断、创建;ps:其实其他数据库,比如mysql、tdengine等,都是同样的道理;

连接数过大

  • 问题:MongoDB 连接数配置较大,导致很多handler服务,过多的线程会导致上下文切换开销变大,同时内存开销也会上涨;
  • 优化:调低连接数配置,进行压力测试;

规则引擎的坑

  • 问题:为了提升性能,往往采用队列+规则引擎来处理业务,设备协议解析+计费业务是在一个handler里,而规则引擎则在下一个队列处理;规则引擎负责为欠费的表具创建短信提醒、关阀指令(产品欠费规则配置),往往在第一个队列里需等待N秒,将规则引擎创建的指令一起下方给表具,每天几百万表具实时上报数据+实时计费,数据并发量较大,如果都等待N秒,会严重影响处理性能;
  • 优化:对计费后余额大于等于0的的表具,不等待N秒,直接回复结束指令,绝大部分表具其实已经是关阀状态,无需本次下发关阀指令,哪怕有需要关阀的,延后到下次通信再执行也没影响;对于小于0的表具,则使用原有逻辑;优化后,降低了90%的等待时间,大大提升高峰高并发处理性能;

HTTP订阅的坑

  • 问题:前期主要通过AEP平台的http订阅实现NB表的通信,但是http服务在高并发时有时候会挂掉(具体表象就是两个http服务,只有一个存活,另一个服务存在,但不处理数据);
  • 优化:将http服务单独服务部署,不要和其他handler、服务抢占服务器资源,jvm也就不会崩了;后期采用MQ订阅方式;

站内信的坑

  • 问题:原有产品设计当设备产生告警时(原生告警、规则告警等)有站内信提醒,经过项目实际运行,站内信数据非常庞大,时刻都有设备告警,导致web页面卡顿、后台web服务资源占用高;
  • 优化:去掉设备站内信告警功能,一是站内信管理人员完全看不过来,二是设备告警在功能菜单里可以查询到;

2024七月5

MongoDB 为什么要限制连接数

Mongod 的服务模型是每个网络连接由一个单独的线程来处理,每个线程配置了1MB 的栈空间,当网络连接数太多时,过多的线程会导致上下文切换开销变大,同时内存开销也会上涨。

  • 连接是要消耗资源的,而且消耗的并不少。
    • 内存:MongoDB为例,每个线程都要分配1MB的栈内存出来。1000个连接,1G内存就这么没了,甭管是否是活跃连接
    • 文件句柄:每个连接都要打开一个文件句柄,当然从成本上讲,这个消耗相对内存是小了很多。但换个角度,文件句柄也被其他模块消耗着,比如WT存储引擎,就需要消耗大量的文件句柄
  • 是否真的需要这么多的链接,一般的业务场景下请求压力在1000QPS左右,按照每个请求50ms计算,最多也就需要1000/(1000/50)==50个链接即可满足需求,并且是整个系统50个链接即可。
  • 很多人平时没有怎么注意过链接数概念,上云后发现居然有这样的限制,心里很不舒服,可能非常不理解。这里说下常见的两种情况:
    • 短链接:一般都是PHP环境,因为PHP的框架决定了PHP短链接的特性,并且链接数的需求一般是在1000-3000左右,具体多少还要根据业务部署的PHP数量来计算。并且MongoDB开源版本在短链接Auth处理上并不优雅,会消耗非常多的CPU资源,3000链接即可跑满24Core的CPU。PHP大拿Facebook也有同样的问题,所以他们用go语言自行开发了一套Proxy代理,来解决对MongoDB的短链接请求问题,但这毕竟带来部署成本和兼容性问题。
    • 长链接:比较健康合理的使用方式,但是也要正确的配置客户端,相关的参数为&maxPoolSize=xx 在ConnectionURI上追加上去即可,否则默认每个客户端就是高处100来个,平白的浪费资源
  • 链接数的上限需要综合考虑性能,稳定性,业务需求。多方面去考虑,缺一不可。超低的内存,配置超高的链接数,得到的只能是OOM。
2024六月20

Odoo 17.0 源码部署的坑

启动需指定配置文件(配置数据库用户和密码,用户不可为默认用户)

python3 odoo-bin -c debian/odoo.conf -i base -d odoo

配置文件

[options]
; This is the password that allows database operations:
; admin_passwd = admin
db_host = 127.0.0.1
db_port = 5432
db_user = odoo
db_password = 1234567890
addons_path = /data/odoo/addons
default_productivity_apps = True

需安装依赖

pip3 install -r requirements.txt

2024六月20

PostgreSQL 常用命令

  • 切换用户

su postgres

pgsql

  • 常用命令

\l 列出数据库

\du 列出用户

\q 退出窗口

  • 修改密码

su – postgres

pgsql

select usename,passwd from pg_shadow;

ALTER USER demo with password ‘demo12345678’;

或创建用户

CREATE USER demo WITH PASSWORD ‘demo12345678’;

赋予权限

ALTER USER demo WITH SUPERUSER;

ALTER USER demo WITH CreateDB;

创建数据库

CREATE DATABASE demo OWNER demo;
2024六月19

企业微信:数据与智能专区 docker打包的坑

1、文档说明要求 python 3版本,但没有说必须是 3.6 版本,其他版本报错找不到 libpython3.6m.so.1.0;

2、使用 python 3.6 得安装 requests 组件,文档没有说明;

3、部署时必须指定执行的 sh 脚本文件;

Dcokerfile 文件内容如下:

FROM python:3.6-slim

WROKDIR /app

COPY . /app

RUN python -m pip install --upgrade pip

RUN pip install requests

RUN pip install pycryptodome

EXPOSE 8080

ps:搞过微信开发的都知道,微信的开发文档到底是多么多么的……

2024六月12

backup:招聘模版

前端开发工程师

职位描述:
1、负责与开发组长沟通业务需求,与后端开发人员对接后端API、开发前端页面;
2、负责修改公司现有软件项目BUG,并对前端功能的持续优化改进;
3、负责公司现有前端框架和组件的维护、改进;

职位要求:
1、2年以上前端开发经验;
2、熟悉Web前端开发基础技能(HTML/CSS/JavaScript);
3、熟悉Vue2、Vue3、ElementUI、Element-Plus等前端开发框架;
4、对前端工程化与模块化开发有一定了解,有webpack/vite/npm实践经验;
5、有GIS开发经验(电子围栏、轨迹、图层等)、数据大屏开发经验者优先;
6、对前端开发有浓厚兴趣,能主动学习;
7、有良好的编码习惯,重视代码质量;
8、善于沟通,工作积极,有责任心,善于协作与分享;

测试运维工程师

岗位职责:
1、负责Java软件项目的全流程测试,包括制定测试计划、编写测试方案和测试用例等;
2、负责Java软件项目的功能、性能等方面的测试工作,执行测试用例,提交BUG,并进行BUG跟踪和回归测试,直到BUG解决;
3、负责Java软件项目的日常更新维护,包括Docker容器化部署等环境;
4、负责收集、跟进客户的使用问题,直到问题被有效解决;

任职资格:
1、本科及以上学历,计算机或相关专业,3年以上软件测试经验;
2、熟悉软件研发、测试流程,了解过程标准和规范,能主动在关键过程节点推动任务执行;
3、熟悉软件测试方法和软件工程知识,流程意识强,具备有效发现问题和解决问题的能力;
4、熟悉Docker部署,熟练掌握Linux服务器的配置和管理;
5、了解常见开源软件的集群化部署、维护和使用,例如RabbitMQ,MongoDB,Redis,MySQL/MariaDB、ElasticSearch等
6、具有一定的文档编写能力,如部署文档、测试方案、测试用例等;

2024一月25

el-table 尾部汇总放在第一行

调用页面添加样式

<style scoped>
::v-deep .el-table {
    display: flex;
    flex-direction: column;
}
::v-deep .el-table__body-wrapper {
    order: 1;
}
::v-deep .el-table__fixed-body-wrapper {
    top: 92px !important;
}
::v-deep .el-table__fixed-footer-wrapper {
    z-index: 0;
    top: 45px;
}
</style>

分页组件完整代码

<template>
    <div class="data-list">
        <transition name="el-fade-in">
            <div v-show="selectRows.length > 0" class="el-selection-bar">
                选择了<span class="el-selection-bar__text">{{ selectRows.length }}</span
                >条数据
                <a class="el-selection-bar__clear" @click="clearSelection">清空</a>
            </div>
        </transition>
        <div v-loading="isRequestIng" class="data-list__table" :element-loading-text="loadingTxt">
            <span v-if="summary">总条数:{{ rows.length }}</span>
            <el-table
                v-bind="$attrs"
                ref="tabList"
                :data="rows"
                v-on="$listeners"
                :border="true"
                :span-method="handleRowSpanMethod"
                :show-summary="summary"
                sum-text=" 汇  总 "
            >
                <slot />
                <pro-empty slot="empty" />
            </el-table>
        </div>
        <div v-if="!summary && (pageData.totalCount > pageData.pageSize || pageData.pageSize > 10)" class="data-list__pager">
            <el-pagination
                :current-page="pageData.pageNo"
                :page-size="pageData.pageSize"
                :total="pageData.totalCount"
                background
                :page-sizes="pageSizes"
                layout="total, ->, prev, pager, next, sizes, jumper"
                @current-change="doChangePage"
                @size-change="doSizeChange"
            />
        </div>

    </div>
</template>

<script>
import { forIn, findIndex, cloneDeep, remove, uniqBy, concat, isArray, first } from "lodash-es"
import { f } from 'vue-marquee-component'
export default {
    name: "PlusTableList",
    props: {
        server: {
            type: String,
            require: true,
            default: ""
        },
        methods: {
            type: String,
            default: "post"
        },
        lazy: {
            type: Boolean,
            default: false
        },
        data: {
            type: Object,
            default: () => {}
        },
        dataFilter: {
            type: Function,
            default: data => data
        },
        loadingTxt: {
            type: String,
            default: "数据加载中..."
        },
        paramClear: {
            type: Boolean,
            default: false
        },
        selectRows: {
            type: Array,
            default: () => []
        },
        selectable: {
            type: Function,
            default: () => true
        },
        rowKey: {
            type: String,
            default: "id"
        },
        selection: {
            type: Boolean,
            default: true
        },
        pageSizes: {
            type: Array,
            default: () => [10, 20, 30, 50]
        },
        // 合并行(第一列)
        spanName0: {
            type: String,
            default: null
        },
        // 合并行(第二列)
        spanName1: {
            type: String,
            default: null
        },
        // 是否汇总数据
        summary: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            pageData: {
                pageNo: 1,
                pageSize: 10,
                totalCount: 0
            },
            rows: [],
            isRequestIng: false
        }
    },
    watch: {
        pageSizes: {
            handler: function (val) {
                if (isArray(val)) {
                    this.pageData.pageSize = first(val)
                }
            },
            immediate: true
        }
    },
    mounted() {
        if (!this.lazy) {
            this.getList()
        }
    },
    methods: {
        // 合并相同值的行
        handleRowSpanMethod({ row, column, rowIndex, columnIndex }) {
            if (columnIndex === 0) {
                if (!this.spanName0) return
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex - 1][this.spanName0]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        if (row[this.spanName0] === this.rows[i][this.spanName0]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
            if (columnIndex === 1) {
                if (!this.spanName1) return
                // 第一列值相同,且第二列值相同的的情况下合并
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex-1][this.spanName0] && row[this.spanName1] === this.rows[rowIndex - 1][this.spanName1]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        // 第一列值相同,且第二列值相同的的情况下合并
                        if (row[this.spanName0] === this.rows[i][this.spanName0] && row[this.spanName1] === this.rows[i][this.spanName1]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
        },
        // 页码变动事件
        doChangePage(val) {
            this.pageData.pageNo = val
            this.getList()
        },
        // 页大小变动事件
        doSizeChange(val) {
            this.pageData.pageSize = val
            this.pageData.pageNo = 1
            this.getList()
        },
        getList() {
            const { totalCount, ...pager } = this.pageData
            const params = Object.assign({}, this.data, pager)
            if (this.paramClear) {
                forIn(params, (value, key) => {
                    if (value === "") delete params[key]
                })
            }
            this.isRequestIng = true
            this.$get(params)
                .then(({ data }) => {
                    this.rows = this.dataFilter(data.list || [])
                    this.pageData.totalCount = data.totalCount
                    this.$emit("updateTotal", data.totalCount)
                    this.isRequestIng = false
                    this.$nextTick(() => {
                        this.handlePageUpdate()
                    })
                })
                .catch(error => {
                    this.isRequestIng = false
                })
        },
        handlePageUpdate() {
            const list = this.rows
            list.forEach(row => {
                if (
                    findIndex(this.selectRows, el => {
                        return el[this.rowKey] === row[this.rowKey]
                    }) !== -1
                ) {
                    this.$refs.tabList.toggleRowSelection(row, true)
                }
            })
        },
        handleSelectionChange(val) {
            const selectRows = cloneDeep(this.selectRows)
            this.$nextTick(() => {
                const list = this.rows.map(el => el[this.rowKey])
                remove(selectRows, el => {
                    return list.includes(el[this.rowKey])
                })
                this.$emit(
                    "update:selectRows",
                    uniqBy(concat(selectRows, val), el => el[this.rowKey])
                )
            })
        },
        $get(data) {
            if (this.methods === "get") {
                return this.$axios.get(this.server, {
                    params: data
                })
            } else {
                return this.$axios.post(this.server, data)
            }
        },
        reset() {
            this.pageData.pageNo = 1
            this.pageData.totalCount = 0
            this.rows = []
            this.clearSelection()
        },
        async clearSelection() {
            this.$refs.tabList.clearSelection()
            await this.$nextTick()
            this.$emit("update:selectRows", [])
        },
        query() {
            this.reset()
            this.getList()
        }
    }
}
</script>

2024一月24

el-table 根据条件合并行

<template>
    <div class="data-list">
        <transition name="el-fade-in">
            <div v-show="selectRows.length > 0" class="el-selection-bar">
                选择了<span class="el-selection-bar__text">{{ selectRows.length }}</span
                >条数据
                <a class="el-selection-bar__clear" @click="clearSelection">清空</a>
            </div>
        </transition>
        <div v-loading="isRequestIng" class="data-list__table" :element-loading-text="loadingTxt">
            <el-table
                v-bind="$attrs"
                ref="tabList"
                :data="rows"
                v-on="$listeners"
                :border="true"
                :span-method="handleRowSpanMethod"
            >
                <slot />
                <pro-empty slot="empty" />
            </el-table>
        </div>
        <div v-if="pageData.totalCount > pageData.pageSize || pageData.pageSize > 10" class="data-list__pager">
            <el-pagination
                :current-page="pageData.pageNo"
                :page-size="pageData.pageSize"
                :total="pageData.totalCount"
                background
                :page-sizes="pageSizes"
                layout="total, ->, prev, pager, next, sizes, jumper"
                @current-change="doChangePage"
                @size-change="doSizeChange"
            />
        </div>
    </div>
</template>

<script>
import { forIn, findIndex, cloneDeep, remove, uniqBy, concat, isArray, first } from "lodash-es"
export default {
    name: "PlusTableList",
    props: {
        server: {
            type: String,
            require: true,
            default: ""
        },
        methods: {
            type: String,
            default: "post"
        },
        lazy: {
            type: Boolean,
            default: false
        },
        data: {
            type: Object,
            default: () => {}
        },
        dataFilter: {
            type: Function,
            default: data => data
        },
        loadingTxt: {
            type: String,
            default: "数据加载中..."
        },
        paramClear: {
            type: Boolean,
            default: false
        },
        selectRows: {
            type: Array,
            default: () => []
        },
        selectable: {
            type: Function,
            default: () => true
        },
        rowKey: {
            type: String,
            default: "id"
        },
        selection: {
            type: Boolean,
            default: true
        },
        pageSizes: {
            type: Array,
            default: () => [10, 20, 30, 50]
        },
        spanName0: {
            type: String,
            default: null
        },
        spanName1: {
            type: String,
            default: null
        }
    },
    data() {
        return {
            pageData: {
                pageNo: 1,
                pageSize: 10,
                totalCount: 0
            },
            rows: [],
            isRequestIng: false
        }
    },
    watch: {
        pageSizes: {
            handler: function (val) {
                if (isArray(val)) {
                    this.pageData.pageSize = first(val)
                }
            },
            immediate: true
        }
    },
    mounted() {
        if (!this.lazy) {
            this.getList()
        }
    },
    methods: {
        // 合并相同值的行
        handleRowSpanMethod({ row, column, rowIndex, columnIndex }) {
            if (columnIndex === 0) {
                if (!this.spanName0) return
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex - 1][this.spanName0]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        if (row[this.spanName0] === this.rows[i][this.spanName0]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
            if (columnIndex === 1) {
                if (!this.spanName1) return
                // 第一列值相同,且第二列值相同的的情况下合并
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex-1][this.spanName0] && row[this.spanName1] === this.rows[rowIndex - 1][this.spanName1]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        // 第一列值相同,且第二列值相同的的情况下合并
                        if (row[this.spanName0] === this.rows[i][this.spanName0] && row[this.spanName1] === this.rows[i][this.spanName1]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
        },
        // 页码变动事件
        doChangePage(val) {
            this.pageData.pageNo = val
            this.getList()
        },
        // 页大小变动事件
        doSizeChange(val) {
            this.pageData.pageSize = val
            this.pageData.pageNo = 1
            this.getList()
        },
        getList() {
            const { totalCount, ...pager } = this.pageData
            const params = Object.assign({}, this.data, pager)
            if (this.paramClear) {
                forIn(params, (value, key) => {
                    if (value === "") delete params[key]
                })
            }
            this.isRequestIng = true
            this.$get(params)
                .then(({ data }) => {
                    this.rows = this.dataFilter(data.list || [])
                    this.pageData.totalCount = data.totalCount
                    this.$emit("updateTotal", data.totalCount)
                    this.isRequestIng = false
                    this.$nextTick(() => {
                        this.handlePageUpdate()
                    })
                })
                .catch(error => {
                    this.isRequestIng = false
                })
        },
        handlePageUpdate() {
            const list = this.rows
            list.forEach(row => {
                if (
                    findIndex(this.selectRows, el => {
                        return el[this.rowKey] === row[this.rowKey]
                    }) !== -1
                ) {
                    this.$refs.tabList.toggleRowSelection(row, true)
                }
            })
        },
        handleSelectionChange(val) {
            const selectRows = cloneDeep(this.selectRows)
            this.$nextTick(() => {
                const list = this.rows.map(el => el[this.rowKey])
                remove(selectRows, el => {
                    return list.includes(el[this.rowKey])
                })
                this.$emit(
                    "update:selectRows",
                    uniqBy(concat(selectRows, val), el => el[this.rowKey])
                )
            })
        },
        $get(data) {
            if (this.methods === "get") {
                return this.$axios.get(this.server, {
                    params: data
                })
            } else {
                return this.$axios.post(this.server, data)
            }
        },
        reset() {
            this.pageData.pageNo = 1
            this.pageData.totalCount = 0
            this.rows = []
            this.clearSelection()
        },
        async clearSelection() {
            this.$refs.tabList.clearSelection()
            await this.$nextTick()
            this.$emit("update:selectRows", [])
        },
        query() {
            this.reset()
            this.getList()
        }
    }
}
</script>