2023年4月

目录

PY32F0系列的封装

在PY32F0系列的封装可以分为两大类, 20PIN及以上的和小于20PIN的.

  • 20PIN, 24PIN 和 32PIN, 带有独立的 NRST 和 BOOT0, PIN脚互相独立不复用;
  • 8PIN, 10PIN 和 16PIN, 没有 BOOT0, 存在多个PIN脚共用同一个物理管脚的情况

这篇主要介绍没有BOOT0的情况如何修改Option Bytes, 以及如何在物理管脚上使用不同的PIN

PY32F002A 的封装

PY32F002AL15S, PY32F002AA15M, PY32F002AW15S

可以看到 SOP8 和 SOP10 存在复用情况



PY32F002AW15U, PY32F002AF15P


PY32F003 的封装

因为PY32F003型号较多, 这里只列出小于20PIN的封装

PY32F003L1xS, PY32F003L2XD, PY32F003L2xS



PY32F003A18N, PY32F003W1XS


PY32F002A/PY32F003 管脚复用

从上面的管脚配置可以看到, 大部分型号都存在同一物理管脚的复用情况, 有一些是功能脚(PF2/NRST)与普通IO脚的复用.

在 OB(Option Bytes)中禁用和启用 PF2/RESET

PF2/NRST这个PIN是比较麻烦的一个功能脚, 因为默认启用了RESET功能, 不受PIN模式的影响, 所以无论你把它设置成INPUT, OUTPUT 还是 ANALOG, RESET永远生效, 和这个PIN同处于同一个物理管脚的PIN就没法正常使用.

要禁用它的RESET功能, 要在芯片的 OB(Option Bytes)里修改. OB 位于地址 0x1FFF 0E80, 占用4个字节, 其中2字节是配置, 另外2字节是这两个字节的反码. 对应 RESET 功能的设置 NRST_MODE 存储于第14位, 0表示仅复位输入, 1表示禁用复位输入,启用 GPIO 功能.

对于正常带 PF4/BOOT0 的型号, 在上电时拉高 BOOT0, 就可以从 system memory 启动 boot loader, 通过 ISP 工具连接后在工具里修改 OB, 但是 SOP8 和 SOP16 这些封装没有 BOOT0, 所以没法使用 ISP 工具修改. 只能通过代码或第三方工具(例如JLink)修改. 以下以LL库为例, 说明在代码中修改OB的方法

在OB中关闭PF2复位输入的方法

static void APP_FlashSetOptionBytes(void)
{
  FLASH_OBProgramInitTypeDef OBInitCfg;

  LL_FLASH_Unlock();
  LL_FLASH_OB_Unlock();

  OBInitCfg.OptionType = OPTIONBYTE_USER;
  OBInitCfg.USERType = OB_USER_BOR_EN | OB_USER_BOR_LEV | OB_USER_IWDG_SW | OB_USER_WWDG_SW | OB_USER_NRST_MODE | OB_USER_nBOOT1;
  /*
   * 默认的值: OB_BOR_DISABLE | OB_BOR_LEVEL_3p1_3p2 | OB_IWDG_SW | OB_WWDG_SW | OB_RESET_MODE_RESET | OB_BOOT1_SYSTEM;
  */
  OBInitCfg.USERConfig = OB_BOR_DISABLE | OB_BOR_LEVEL_3p1_3p2 | OB_IWDG_SW | OB_WWDG_SW | OB_RESET_MODE_GPIO | OB_BOOT1_SYSTEM;
  LL_FLASH_OBProgram(&OBInitCfg);

  LL_FLASH_Lock();
  LL_FLASH_OB_Lock();
  /* 重新载入OB, 这会触发软复位, MCU重启 */
  LL_FLASH_OB_Launch();
}

注意, 上面这个方法执行后会重启MCU, 所以在调用前要做个判断, 否则它会一直循环重启下去

/* 检查 PF2 是否已经关闭了复位 */
if(READ_BIT(FLASH->OPTR, FLASH_OPTR_NRST_MODE) == OB_RESET_MODE_RESET)
{
  /* 如果没关闭则调用 */
  APP_FlashSetOptionBytes();
}
// 否则继续正常执行

这样执行完之后, RESET按钮就失效了, 如果要恢复, 要再将OB改回默认的值

OB_BOR_DISABLE | OB_BOR_LEVEL_3p1_3p2 | OB_IWDG_SW | OB_WWDG_SW | OB_RESET_MODE_RESET | OB_BOOT1_SYSTEM;

同一物理管脚的其它PIN, 设为模拟(ANALOG)模式

以下以SOP16封装的为例, 启用 PF1, PF0, 禁用对应同一管脚的 PA14 和 PF2

static void APP_GPIO_Config(void)
{
  //...

  // PF1 SCL
  GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_OPENDRAIN;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_12;
  LL_GPIO_Init(GPIOF, &GPIO_InitStruct);

  // PF0 SDA
  GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_12;
  LL_GPIO_Init(GPIOF, &GPIO_InitStruct);

  /**
   * 根据数据手册第20页, 同管脚的其它PIN应当设为 ANALOG.
  */
  // PA14
  LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_14, LL_GPIO_MODE_ANALOG);
  // PF2
  LL_GPIO_SetPinMode(GPIOF, LL_GPIO_PIN_2, LL_GPIO_MODE_ANALOG);

  //...
}

电路连线避免干扰

管脚复用之后, 一些功能脚带的开关按钮和电阻电容就会对其它PIN造成影响.

例如对于复位键, 如果上面加了电容, 其容量一般是104(100nF), 用于避免按键抖动, 如果将这个脚禁用复位, 改为I2C的输出, 这个电容就会对输出信号造成干扰, 100nF的容量基本能消除掉1KHz以上的频率, 所以要将这样的电容去掉.

启动增加延时, 确保上电烧录

因为小封装没有 BOOT0, 所以在 SWD 口烧录失败的情况下, 没法用 ISP 工具救场, 如果你的程序加电后没有预留足够长时间的 delay, 又把 SWD 口的 PA13 PA14 给关掉了, 那下一次烧录就会干瞪眼.

一个好习惯是在设置完时钟之后, 保留一到两秒的延时, 可以在加电后从容不迫地按下烧录按钮.

int main(void)
{
  uint8_t i;

  BSP_RCC_HSI_24MConfig();
  /** 
   * 在SWD口关闭前停留2秒, 保证上电后有足够长的烧录等待时间
  */
  LL_mDelay(2000);

  //...

代码示例

以 SOP16 封装的 PY32F003W18S 为例, 依然使用 1602LCD 作为参考.

代码通过禁用 PA14 和 PF2, 将 PF1 和 PF0 设置为 I2C 外设接口, 驱动 1602LCD.

源代码已经提交到 GitHub 仓库, 地址:
https://github.com/IOsetting/py32f0-template/tree/main/Examples/LL/I2C/PCF8574_1602LCD_PY32F003W_PF0_PF1

运行示例

计算的字段和变更(Computed Fields And Onchanges)

模型之间的关系是任何Odoo模块的关键组成部分。它们对于任何业务案例的建模都是必要的。然而,我们可能需要给定模型中字段之间的链接。有时,一个字段的值是根据其他字段的值确定的,有时我们希望帮助用户输入数据。

“Computed Fields And Onchanges”的概念支持这些情况。虽然本章在技术上并不复杂,但这两个概念的语义都非常重要。这也是我们第一次编写Python逻辑。到目前为止,除了类定义和字段声明之外,我们还没有编写任何其他东西。

计算的字段(Computed Fields)

参考
: 主题关联文档可查阅
Computed Fields
.

本章目标

  • 在房地产模型中,自动计算总的面积和最佳报价

预期效果:

img

  • 在地产报价模型中,自动计算合法的日期且可被更新

预期效果:

img

在我们的房地产模块中,我们定义了生活区和花园区。自然地我们将总面积定义这两者的总和,我们将为此使用计算的字段的概念,即给定字段的值将从其他字段的值中计算出来。

到目前为止,字段已直接存储在数据库中并直接从数据库中检索。字段也可以被计算。在这种情况下,不会从数据库中检索字段的值,而是通过调用模型的方法来动态计算的字段的值。

要创建计算的字段,请创建字段并将其属性
compute
设置为方法的名称。计算方法应为
self
中的每个记录设置计算的字段的值。

按约定,
compute
方法是私有的,这意味着它们不能从表示层调用,只能从业务层调用。私有方法的名称以下划线
_
开头。

依赖(Dependencies)

计算的字段的值通常取决于计算记录中其他字段的值。ORM期望开发人员使用修饰符
depends()
指定计算方法上的依赖项。每当修改字段的某些依赖项时,ORM使用给定的依赖项来触发字段的重新计算

from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

注解

self
是一个集合

self
对象是一个结果集(
recordset
),即一个有序记录集合。支持标准Python集合运算,比如
len(self)

iter(self)
, 外加其它集合操作,比如
recs1 | recs2


self
上迭代,会一个接一个的生成记录,其中每个记录本身是长度为1的集合。可以使用
.
(比如
record.name
)访问单条记录的字段或者给字段赋值。

一个简单的示例

    @api.depends('debit', 'credit')
    def _compute_balance(self):
        for line in self:
            line.balance = line.debit - line.credit
练习--计算总面积
  • 添加
    total_area
    字段到
    estate.property
    。该字段被定义为
    living_area

    garden_area
    的总和。
  • 添加字段到表单视图,正如本章目标中展示的那样

对于关系型字段,可以使用通过字段的路径作为依赖项:

description = fields.Char(compute="_compute_description")
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

示例以
Many2one
为例,针对
Many2many
或者
One2many
一样的。

一个简单的示例

    @api.depends('line_ids.amount_type')
    def _compute_show_decimal_separator(self):
        for record in self:
            record.show_decimal_separator = any(l.amount_type == 'regex' for l in record.line_ids)

修改
odoo14\custom\estate\models\estate_property.py

修改

from odoo import models, fields

from odoo import models, fields, api

最末尾添加以下内容

    total_area = fields.Integer(compute='_compute_total_area')

    @api.depends("garden_area, living_area")
    def _compute_total_area(self):
        for record in self:
            record.total_area = record.living_area + record.garden_area

修改
odoo14\custom\estate\views\estate_property_views.xml

estate_property_view_form
视图,
Description
描述页,添加
total_area
字段

                        <page string="Description">
                            <group>
                                <field name="description"></field>
                                <field name="bedrooms"></field>
                                <field name="living_area"></field>
                                <field name="facades"></field>
                                <field name="garage"></field>
                                <field name="garden"></field>
                                <field name="garden_area"></field>
                                <field name="garden_orientation"></field>
                                <field name="total_area" string="Total Area"></field><!--本次添加的内容-->
                            </group>
                        </page>

重启服务,刷新浏览器验证效果


)

练习--计算最佳报价
  • 添加
    best_price
    字段到
    estate.property
    。该字段被定义为最高报价
  • 添加该字段到表单视图,正如本章目标中的第一个动画

提示:你可能会想用
mapped()
方法,查看
示例

                writeoff_amount = sum(writeoff_lines.mapped('amount_currency'))

修改
odoo14\custom\estate\models\estate_property.py
,在
total_area
下方添加
best_price

    best_price = fields.Float(compute='_compute_best_offer')

最末尾添加以下函数

    @api.depends('offer_ids.price')
    def _compute_best_offer(self):
        for record in self:
            prices = record.mapped('offer_ids.price')
            if prices:
                record.best_price = max(prices)
            else:
                record.best_price = 0.00

修改
odoo14\custom\estate\views\estate_property_views.xml
文件
estate_property_view_form
视图

                        <group>
                            <field name="expected_price" string="Expected Price"></field>
                            <field name="selling_price" string="Selling Price"></field>
                        </group>

修改为

                        <group>
                            <field name="expected_price" string="Expected Price"></field>
                            <field name="best_price" string="Best Price" />
                            <field name="selling_price" string="Selling Price"></field>
                        </group>

重启服务,验证效果(参考本章目标中第一个动画连接)

Inverse函数

你可能已经注意到,计算的字段默认总是只读的。这正是我们期望的,因为不支持用户设置值。

某些情况下,可以直接设置值可能会很有用。在我们的房产示例中,我们可以定义报价的有效期间并设置有效日期。我们希望能够设置有效期间或日期,并且两者之间相互影响。

为了支持这个需求,odoo提供了使用
inverse
函数的能力:

from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total", inverse="_inverse_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

    def _inverse_total(self):
        for record in self:
            record.amount = record.total / 2.0

一个简单的示例

    @api.depends('partner_id.email')
    def _compute_email_from(self):
        for lead in self:
            if lead.partner_id.email and lead.partner_id.email != lead.email_from:
                lead.email_from = lead.partner_id.email

    def _inverse_email_from(self):
        for lead in self:
            if lead.partner_id and lead.email_from != lead.partner_id.email:
                lead.partner_id.email = lead.email_from

compute方法设置字段,而inverse方法设置字段的相关性。

注意,保存记录时调用
inverse
方法,而每次更改依赖项时调用
compute
方法。

练习--为报价计算一个有效期
  • 添加以下字段到
    estate.property.offer
    模型:
Field Type Default
validity Integer 7
date_deadline Date

其中,
date_deadline
为一个计算的字段,定义为
create_date

validity
两个字段的和。定义一个适当的
inverse
函数这样,以便用户可以编辑
create_date

validity

提示:
create_date
仅在记录创建时被填充,因此需要一个回退,防止创建时的奔溃

  • 在表单和列表视图中添加字段,正如本章目标中显示的第二个动画中的一样。

修改
odoo14\custom\estate\models\estate_property_offer.py

from odoo import models, fields

修改为

from odoo import models, fields, api
from datetime import timedelta

末尾添加以下代码

    validity = fields.Integer(default=7)
    date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline')

    @api.depends('validity', 'create_date')
    def _compute_date_deadline(self):
        for record in self:
            if record.create_date:
                record.date_deadline = record.create_date.date() + timedelta(days=record.validity)
            else:
                record.date_deadline = datetime.now().date() + timedelta(days=record.validity)

    @api.depends('validity', 'create_date')
    def _inverse_date_deadline(self):
        for record in self:
            if record.create_date:
                record.validity = (record.date_deadline - record.create_date.date()).days
            else:
                record.validity = 7

修改
odoo14\custom\estate\views\estate_property_offer_views.xml

<?xml version="1.0"?>
<odoo>
    <record id="estate_property_offer_view_tree" model="ir.ui.view">
        <field name="name">estate.property.offer.tree</field>
        <field name="model">estate.property.offer</field>
        <field name="arch" type="xml">
            <tree string="PropertyOffers">
                <field name="price" string="Price"/>
                <field name="partner_id" string="partner ID"/>
                <field name="validity" string="Validity(days)"/>
                <field name="date_deadline" string="Deadline"/>
                <field name="status" string="Status"/>
            </tree>
        </field>
    </record>
    <record id="estate_property_offer_view_form" model="ir.ui.view">
        <field name="name">estate.property.offer.form</field>
        <field name="model">estate.property.offer</field>
        <field name="arch" type="xml">
            <form string="estate property offer form">
                <sheet>
                    <group>
                        <field name="price" string="Price"/>
                        <field name="validity" string="Validity(days)"/>
                        <field name="date_deadline" string="Deadline"/>
                        <field name="partner_id" string="partner ID"/>
                        <field name="status" string="Status"/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>
</odoo>

重启服务,浏览器中验证(参考本章目标中的第二个动画视图)

其它信息

默认的,计算的字段不会存到数据库中,因此,不可能基于计算的字段进行搜索,除非定义一个
search
方法。该主题不在训练范围内,所以,这里不做介绍。
一个简单示例

    is_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing')

另一个解决方法是使用
store=True
属性存储该字段。虽然这通常很方便,但请注意给模型增加的潜在计算压力。让我们重新使用我们的示例。复用我们的示例:

description = fields.Char(compute="_compute_description", store=True)
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

每次partner
name
被改变, 自动为所有引用了它的记录更新
description
当数以百万计的记录需要重新计算时,这可能会很快会变得无法承受

还值得注意的是,计算的字段可以依赖于另一个计算的字段。ORM足够聪明,可以按照正确的顺序正确地重新计算所有依赖项……但有时会以降低性能为代价。

通常,在定义计算的字段时,必须始终牢记性能。要计算的字段越复杂(例如,具有大量依赖项或当计算的字段依赖于其他计算的字段时),计算所需的时间就越长。请务必事先花一些时间评估计算的字段的成本。大多数时候,只有当您的代码到达生产服务器时,你才意识到它会减慢整个过程。

Onchanges

参考
: 主题关联文档可查看
onchange()
:

在我们的房地产模块中,我们还想帮助用户输入数据。设置“garden”字段后,我们希望为花园面积和朝向提供默认值。此外,当“花园”字段未设置时,我们希望花园面积和重置为零,并删除朝向。在这种情况下,给定字段的值会影响其他字段的值。

“onchange”机制为客户端界面提供了一种,无论用户合适填写字段值更新表单,都无需存储任何东西到数据库的一种方法。为了实现这一点,我们定义了一个方法,其中
self
表示表单视图中的记录,并用
onchange()
修饰该方法,以指明它由哪个字段触发。你对
self
所做的任何更改都将反映在表单上:

from odoo import api, fields, models

class TestOnchange(models.Model):
    _name = "test.onchange"

    name = fields.Char(string="Name")
    description = fields.Char(string="Description")
    partner_id = fields.Many2one("res.partner", string="Partner")

    @api.onchange("partner_id")
    def _onchange_partner_id(self):
        self.name = "Document for %s" % (self.partner_id.name)
        self.description = "Default description for %s" % (self.partner_id.name)

这个例子中,修改partner的同时也将改变名称和描述值。最终取决于用户是否修改名称和描述值。 同时,需要注意的是,不要循环遍历
self
,因为该方法在表单视图中触发,
self
总是代表单条记录。

练习--为花园面积和朝向赋值


estate.property
模型中创建
onchange
方法以便当勾选花园时,设置花园面积(10)和朝向(North),未勾选时,移除花园面积和朝向值。

修改
odoo14\custom\estate\models\estate_property.py
,末尾添加一下代码

    @api.onchange("garden")
    def _onchange_garden(self):
        if self.garden:
            self.garden_area = 10
            self.garden_orientation = 'North'
        else:
            self.garden_area = 0
            self.garden_orientation = ''

重启服务,验证效果(预期效果参考动画:
https://www.odoo.com/documentation/14.0/zh_CN/_images/onchange.gif
)

其它信息

Onchanges方法也可以返回非阻塞告警消息(
示例
)

    @api.onchange('provider', 'check_validity')
    def onchange_check_validity(self):
        if self.provider == 'authorize' and self.check_validity:
            self.check_validity = False
            return {'warning': {
                'title': _("Warning"),
                'message': ('This option is not supported for Authorize.net')}}

如何使用它们?

对于computed field 和Onchanges的使用没有严格的规则。

在许多情况下,可以使用computed field和onchanges来实现相同的结果。始终首选computed field,因为它们也是在表单视图上下文之外触发的。永远不要使用onchange将业务逻辑添加到模型中。这是一个
非常糟糕的
想法,因为在以编程方式创建记录时不会自动触发onchanges;它们仅在表单视图中触发。

computed field和onchanges的常见陷阱是试图通过添加过多逻辑来变得“过于智能”。这可能会产生与预期相反的结果:终端用户被所有自动化所迷惑。

computed field往往更容易调试:这样的字段是由给定的方法设置的,因此很容易跟踪设置值的时间。另一方面,onchanges可能会令人困惑:很难知道onchange的程度。由于几个onchange方法可能会设置相同的字段,因此跟踪值的来源很容易变得困难。

存储computed fields时,请密切注意依赖项。当计算字段依赖于其他计算字段时,更改值可能会触发大量重新计算。这会导致性能不佳。

众所周知,流水线技术对于软件开发人员不是
可见的(visiable)
,毕竟已经在在机器语言之下,是组成机器语言的基本逻辑

但今天我就带领大家看看我新发现的结果,那就是流水线的
可视效果
,包括流水线预测技术的侧面体现,当然也是可见的

首先我先声明一下需要的基础,需要懂16位以及32位操作系统下的汇编语言,不懂者当然是不能体会到这篇文章的意图的

知识点来源
:x86汇编语言从实模式到保护模式(
希望以后大家写博客能标出知识点来源,帮助小白纠正知识是从博客学来的坏习惯,以及给大家一个深入学习的机会
)

好了,直接开始看代码(全汇编)

1    moveax,cr02    or eax,1 
3    mov cr0,eax ;设置PE位    并以32位模式开始译码
4    
5    ;以下进入保护模式... ...
6    ;jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
7    
8    ;清流水线并串行化处理器
9    [bits 32];将之后的代码全部解析成32位模式代码
10    flush: 
11    mov cx,0100 ;加载数据段选择子(0x10)

1-3行就是将16位实模式转换到32位保护模式,并且将16位下的译码模式转换成32位的译码模式

第6行的代码是跳转到flush标签,也就是第10行,不过我在这里先行给它注释

第9行是一条对于编译器有作用的标签,和C语言中的
#pragma comment
差不多,这里的意思是之后的代码全部编译成32位的汇编

下面是运行时的反汇编代码

这两条连续的反汇编指令是从汇编代码中的第三行开始的,我把汇编代码以及机器指令一并圈了出来

大家有没有发现不对劲,第二条反汇编指令和我们手写的汇编指令不能说是看不出来,只能说是毫无关联,

明明我们的操作数是cx,这边竟然变成了ecx,还有立即数,明明是0x0010,这边变成了0xd98e0010,也不怪大家看不出来,这换谁都一样

大家都知道,机器是很傻的,我们叫他干啥,他就干啥,这边为什么就没有听我们的话呢

其实就是因为计算机流水线的缘故,大家可以回忆一下流水线的特性(不会等一条指令执行完了,再取下一条指令)

我们再仔细观察一下第二条指令的机器代码, 66 b9 10 00 8e d9

66这这机器指令前缀的作用是16位模式下,将16位寄存器转成32位寄存器,

32位模式下,将32位寄存器转成16位寄存器,

因为32位和16位下的代表寄存器的机器码是一样的,就比如16位下cx是d9,而32位模式下ecx也是d9,那总得有机器码在32位下表示cx吧

就拿上面这条66 b9 10 00 8e d9 举例

32位模式下这条指令是 :mov cx,0x0010(8e,d9之所以没了,就是因为cx装不下,本来这就是下一条指令的内容)

而16位模式下就是:      mov ecx,0xd98e0010

很明显啊,我们的代码使用了16位下的解码方式,但是我们明明已经进入了保护模式

而究其原因就是计算机流水线的缘故了,很明显是我们在mov cr0,eax之前就开始译码(decode)了mov cx,0x0010,而且是以16位模式译码,直接导致了程序的失控

现在我们把汇编代码第6行的注释打开,让jmp生效(虽然我的学过的流水线遇到jmp是不会导致流水线暂停的,但有可能我虚拟机模拟的硬件太老)

可以看到,这下反汇编指令就是完全正确的了,也的确说明了流水线发生了暂停,不然我们解码肯定会以16位来解码

一、概述

二阶段消息是
DTM
新提出的,可以完美代替现有的事务消息和本地消息表架构。无论从复杂度、性能、便利性还是代码量都是完胜现有的方案。

相比现有的消息架构借助于各种消息中间件比如
RocketMQ
等,
DTM
自己实现了无需额外的学习成本。它能够保证本地事务的提交和全局事务提交是“原子的”,
适合解决不需要回滚的分布式事务场景

二阶段消息保证提交的原子性和如何保证业务成功执行如下时序图:

二阶段消息主要是指
Prepare

Submit
两个阶段,主程序向
DTM
服务发送
Prepare
消息,成功后执行本地事务,完成本地事务后发送
Submit
消息至
DTM
服务,之后
DTM
会调用分支事件执行其他服务,最后完成全局事务。

当发送了
Prepare
但是
Submit
没有提交的话,会进行回调请求来确认消息的情况,具体工作过程如下:

1、在处理本地事务时,会将
gid
插入到
barrier
表中,同时带上插入原因为
committed
。该表有一个唯一索引,主要字段为
gid

2、当进行回查时,二阶段消息的操作不是直接查
gid
是否存在,而是再
insert ignore
一条带有相同
gid
的数据,同时带上插入原因为
rollbacked
。此时如果表中如果已有
gid
的记录,那么新的插入操作就会被
ignore
,否则数据会被插入。

3、然后再用
gid
查询表中的记录,如果查到记录的
reason

committed
,那么说明本地事务已提交;如果查到记录的
reason

rollbacked
,那么说明本地事务已回滚。

二、安装DTM

我使用二进制包下载安装
地址
,我是
Window
环境所以下载后解压,点击
dtm.exe
进行运行即可,如下启动成功

启动成功后可以访问
http://localhost:36789
,进入管理后台

三、创建DTM所需的表

我们需要创建一个表处理消息的回查,表里保存全局事务ID,具体作用在后续说明,我这里用的SqlServer数据库,所以执行如下:

CREATE TABLE [dbo].[barrier]([id] bigint NOT NULL IDENTITY(1,1) PRIMARY KEY,[trans_type] varchar(45) NOT NULL DEFAULT(''),[gid] varchar(128) NOT NULL DEFAULT(''),[branch_id] varchar(128) NOT NULL DEFAULT(''),[op] varchar(45) NOT NULL DEFAULT(''),[barrier_id] varchar(45) NOT NULL DEFAULT(''),[reason] varchar(45) NOT NULL DEFAULT(''),[create_time] datetime NOT NULL DEFAULT(getdate()) ,[update_time] datetime NOT NULL DEFAULT(getdate())
)
GO CREATE UNIQUE INDEX[ix_uniq_barrier] ON[dbo].[barrier]([gid] ASC, [branch_id] ASC, [op] ASC, [barrier_id] ASC)WITH(IGNORE_DUP_KEY = ON)GO

这里比较关键的是那个唯一索引,有一个
IGNORE_DUP_KEY = ON
,这个其实就是为了等价
mysql

insert ignore
表示存在相关字段的信息则不插入,否则就插入数据

当然还支持很多其他的数据库,建表语句可以从这里查看
地址

四、创建项目

我们简单的创建两个
.net core webapi
项目进行测试,两个项目都进行相同的如下操作:

1、安装Dtmcli和Microsoft.EntityFrameworkCore.SqlServer

安装
Dtmcli
是因为其中已经帮我们集成了
DTM
客户端
SDK HTTP
版本,想要
GRPC
版本可以安装
Dtmgrpc

安装
Microsoft.EntityFrameworkCore.SqlServer
很显然是为了处理数据库。

Install-Package Dtmcli
Install
-Package Microsoft.EntityFrameworkCore.SqlServer

2、配置

接下来我们配置服务,先在配置文件
appsetting.json
中添加如下

  "AppSettings": {"DtmUrl": "http://localhost:36789","BusiUrl": "http://localhost:5056","QueryPreparedUrl": "http://localhost:5046","BarrierConn": "Data Source=.;Initial Catalog=HTGL;TrustServerCertificate=True;;Integrated Security=True"}

DtmUrl

DTM
的监听地址,
http
的是
36789

grpc
的是
36790

BusiUrl
:访问其他服务的地址

QueryPreparedUrl
:回查的地址

BarrierConn
:数据库连接语句

添加一个配置类:

    public classAppSettings
{
public string DtmUrl { get; set; }public string BusiUrl { get; set; }public string BarrierConn { get; set; }public string QueryPreparedUrl { get; set; }
}

之后注入服务如下:

builder.Services.AddDtmcli(dtm =>{ 
dtm.DtmUrl
= builder.Configuration.GetValue<string>("AppSettings:DtmUrl");
dtm.SqlDbType
=DtmCommon.Constant.Barrier.DBTYPE_SQLSERVER;
dtm.BarrierSqlTableName
= "[HTGL].[dbo].[barrier]";
});
builder.Services.Configure
<AppSettings>(builder.Configuration.GetSection("AppSettings"));

SqlDbType
:表示使用的数据库类型

BarrierSqlTableName

Barrier
表的名字

3、添加代码

我们在其中一个项目添加主程序代码如下:

[ApiController]public classDtmController : ControllerBase
{
private readonly ILogger<DtmController>_logger;private readonlyIDtmClient _dtmClient;private readonlyIDtmTransFactory _transFactory;private readonlyAppSettings _settings;private readonlyIBranchBarrierFactory _factory;public DtmController(ILogger<DtmController> logger, IDtmClient dtmClient,IDtmTransFactory transFactory, IOptions<AppSettings>settings, IBranchBarrierFactory factory)
{
_logger
=logger;
_dtmClient
=dtmClient;
_transFactory
=transFactory;
_settings
=settings.Value;
_factory
=factory;
}
private DbConnection GetConn() => newMicrosoft.Data.SqlClient.SqlConnection(_settings.BarrierConn);
[HttpPost(
"post-dtm-msg")]public async Task<IActionResult>Get(CancellationToken cancellationToken)
{
//1、创建gid var gid = await_dtmClient.GenGid(cancellationToken);//2、设置分支事务 var msg =_transFactory.NewMsg(gid)
.Add(_settings.BusiUrl
+ "/TransOut", new { id = 123})
.Add(_settings.BusiUrl
+ "/TransIn", new { id = 321});//3、执行submit using (DbConnection conn =GetConn())
{
await msg.DoAndSubmitDB(_settings.QueryPreparedUrl + "/msg-queryprepared", conn, async tx =>{//4、执行本地事务 awaitTask.CompletedTask;
});
}
_logger.LogInformation(
"result gid is {0}", gid);return Content("SUCCESS");
}
[HttpGet(
"msg-queryprepared")]public async Task<IActionResult>QueryPrepared(CancellationToken cancellationToken)
{
var bb =_factory.CreateBranchBarrier(Request.Query);
_logger.LogInformation(
"bb {0}", bb);using (DbConnection conn =GetConn())
{
//回调查询消息状态 var res = awaitbb.QueryPrepared(conn);return Ok(new { dtm_result =res });
}
}
}

然后我们向另一个服务项目添加如下代码,作为一个简单的服务方法,没有任何操作只是返回成功:

[ApiController]public classTransController : ControllerBase
{
private readonly ILogger<TransController>_logger;private readonlyIBranchBarrierFactory _factory;private readonlyAppSettings _settings;private DbConnection GetConn() => newMicrosoft.Data.SqlClient.SqlConnection(_settings.BarrierConn);public TransController(ILogger<TransController> logger, IBranchBarrierFactory factory, IOptions<AppSettings>settings)
{
_logger
=logger;
_factory
=factory;
_settings
=settings.Value;
}
[HttpPost(
"TransIn")]public async Task<IResult>In()
{
return Results.Ok(new { dtm_result = "SUCCESS"});//return Results.Ok(new { dtm_result = "FAILURE" }); }
[HttpPost(
"TransOut")]public async Task<IResult>Out()
{
return Results.Ok(new { dtm_result = "SUCCESS"});
}
}

五、执行查看结果

我们正常执行,可以看到下面的动图结果,在执行完本地事务后会访问分支事务,然后数据库表中添加了一条记录

可以在管理后台看到我们请求成功的信息

如果要演示失败,需要做以下修改直接报错,我们可以看到访问了回调方法,然后数据库中看到
rollback
标记的消息

using (DbConnection conn =GetConn())
{
await msg.DoAndSubmitDB(_settings.QueryPreparedUrl + "/msg-queryprepared", conn, async tx =>{throw new Exception("报错了");//4、执行本地事务 awaitTask.CompletedTask;
});
}

提交后再宕机演示比较麻烦,我就不演示了,大家意会即可。

如果分支事务返回的不是SUCCESS而是FAILURE会由DTM隔一段时间重新请求,dtm对每个事务的重试是指数退避策略,具体为间隔是每失败一次,间隔加倍,避免过多的重试,导致系统负载异常上升。

如果您经过长时间的的宕机,因指数退避算法导致要很久才会重试。如果您想要手动触发立即重试,您可以手动把相应事务的next_cron_time(Redis存储引擎的该功能还在开发中)修改为当前时间,就会在数秒内被定时轮询,事务就会继续往前执行。

背景

手头的ThinkPad在近一年的时间里每次升级Windows 11的22h2版本每次都会报错,具体有以下几种情况:

  1. 更新过程中无问题,重启后黑屏更新过程中会卡在26%左右,然后蓝屏报
    KENERAL_CHECK_FAIL
    ,接着便自动重启进入修复程序
  2. 在Windows Update更新中报错
    0xC1900101
  3. 在上述错误出现后,再次更新会出现
    0x80248007

    0x80248014
    等报错拒绝更新,此类错误代码有很多,刷新一次有一个问题,但是无法更新就是了

上述问题已经以下方法,均无果:

  • 运行Windows疑难问题解答修复Windows Update
  • 删除Windows更新缓存重新加载
  • 使用官方提供的Windows 11 22h2镜像装载并进行完整更新

解决方案

后来我在经过如下一整套操作后,此问题得以解决:

  • 关闭BitLocker加密功能并解除系统盘已有的加密(此步骤一定要先执行,否则后面对BIOS进行操作后检测到安全程序变更会要求BitLocker密钥认证,如果忘记了会很麻烦)
  • 进入BIOS,找到
    Inter virtualization

    virtual dma kernel
    选项将其置为
    disable
    状态(也有人说只关闭dma即可,但是对我没有作用)

Security-Virtualization

  • 接着再到Boot选项内,
    暂时
    关闭Security boot

接着保存并重启,以上问题即可解决。

根据Reddit论坛Lenovo用户反馈,Lenovo品牌系列电脑均可用此方式尝试解决,work for many lenovo devices.

提示

虚拟化功能关闭后,更新虽然成功但是WSL等虚拟化服务功能会全部无法使用,如果有开机自动运行的服务可能会有问题,所以在更新结束后建议及时将上述关闭功能重新
enable
,经实测不影响22h2系统正常使用以及后续的patch。

总结

建议Lenovo能够真正找点工程师解决一下这类问题,别的品牌电脑都没有类似问题,但是找Lenovo中国工程师总是车轱辘话来回说,后来直接让我重置系统安装...那成百上千的环境您能给我重新配吗- -

好在最终找到了解决方案,谨以此分享给所有更新失败的Lenovo用户。