使用go语言构建区块链 Part4.事务1

news2024/11/24 21:55:48

英文源地址

简介

事务是比特币的核心, 区块链的唯一目的是以安全可靠的方式存储交易, 因此在交易创建后没有人可以修改. 今天我们开始实现事务, 但由于这是一个相当大的主题, 我将它分成两部分: 在这一部分中, 我们将实现事务的通用机制, 在第二部分中, 我们将研究细节.
此外, 由于代码的变化是巨大的, 在这里描述它们是没有意义的. 你可以在这里查看到所有的变化.

There is no spoon(黑客帝国台词)

如果你曾经开发过一个web应用程序, 为了实现支付, 你可能会在数据库中创建这些表: 账户和交易.一个账户将存储有关用户的信息, 包括他们的个人信息和余额, 一次交易将存储一个账户到另一个账户的资金转移的信息. 在比特币中, 交易以完全不同的方式实现. 它们是:

  1. 没有账户
  2. 没有余额
  3. 没有地址
  4. 没有硬币
  5. 没有发送者和接收者

由于区块链是一个公开和开放的数据库, 我们不想存储关于钱包所有者的任何敏感信息.硬币不在账户中收纳. 交易不会把钱从一个地址转移到另一个地址.没有保存账户余额的字段或属性. 只有交易, 但是交易里面有什么呢?

比特币交易
交易是输入和输出的组合

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

对于每一笔新的交易, 它的输入会引用前一笔交易的输出(这里有个例外, coinbase交易), 引用就是花费的意思.所谓引用之前的一个输入, 也就是将之前的一个输出包含在另一笔交易的输入当中, 就是花费之前的交易输出. 交易的输出, 就是币实际存储的地方. 下面的图示阐释了交易之间的互相关联.
在这里插入图片描述
需要注意的是:

  1. 有一些输出并没有被关联到某个输入上
  2. 一笔交易的输入可以引用之前多笔交易的输出
  3. 一个输入必须引用一个输出

贯穿全文, 我们将会使用像’money’, ‘coin’, ‘spend’, ‘send’, 'account’等等这样的词. 但是在比特币中, 其实并不存在这些概念. 交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value), 而这些值只可以被锁定它们的人解锁(unlock).
(每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。)

交易输出

先从输出开始

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

输出主要包含两部分:

  1. 一定量的比特币(Value)
  2. 一个锁定脚本(ScriptPubKey), 要花这笔钱, 必须解锁该脚本

实际上, 正式输出里面存储了’币’(注意, 也就是上面的Value字段). 而这里的存储, 指的是用一个数学难题对输出进行锁定, 这个难题被存储在ScriptPubKey里面. 在内部, 比特币使用了一个叫做Script的脚本语言, 用它来定义锁定和解锁输出的逻辑. 虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之), 并不复杂, 但是我们也并不会讨论它的细节. 你可以在这里找到详细解释.

在比特币中, value字段存储的是satoshi的数量, 而不是BTC的数量. 一个satoshi等于一亿分之一的BTC(0.00000001BTC), 这也是比特币里面最小的货币单位(就像是一分的硬币)

由于没有实现地址(address), 所以目前我们会避免涉及逻辑相关的完整脚本. ScriptPubKey将会存储一个任意的字符串(用户定义的钱包地址).

顺表说一下, 有了一个这样的脚本语言, 也意味着比特币其实可以作为一个智能合约平台.

关于输出, 非常重要的一点是: 它们是不可再分的(indivisible). 也就是说, 你无法仅引用其中的一部分. 要么不用, 如果要用, 必须一次性用完. 当一个新的交易中引用了某个输出, 那么这个输出必须被全部花费. 如果它的值比需要的值大, 那么就会产生一个找零, 找零会返还给发送方.这跟现实世界的场景十分类似, 当你像要支付时, 如果一个东西值1美元, 而你给了一个5美元的纸币, 那么你会得到一个4美元的找零.

交易输入

这里是输入

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

正如之前所提到的, 一个输入引用了一个输出: Txid存储的时之前交易的ID, Vout存储的时该输出在那笔交易中所有输出的索引(因为一笔交易可以有多个输出, 需要有信息指明时具体的哪一个).ScriptSig是一个脚本, 提供了可解锁输出结构里面ScriptPubKey字段的数据. 如果ScriptSig提供的数据是正确的, 那么输出就会解锁, 然后被解锁的值可以被用于产生新的输出; 如果数据不正确, 输出就无法被引用在输入中, 或者说, 无法使用这个输出. 这种机制, 保证了用户无法花费属于其他人的币.
再次强调, 由于我们还没有实现地址, 所以目前ScriptSig将仅仅存储一个用户自定义的任意钱包地址. 我们会在下一篇文章中实现公钥(public key)和签名(signature).
来简要总结一下.输出, 就是’币’存储的地方. 每个输出都会带有一个解锁脚本, 这个脚本定义了解锁该输出的逻辑. 每笔新的交易, 必须至少有一个输入与输出. 一个输入引用了之前一笔的输出, 并提供了解锁数据(也就是ScriptSig字段), 该数据会被用于在输出的解锁脚本中解锁输出, 解锁完成后即可使用它的值去产生新的输出.
每一笔输入都是之前一笔交易的输出, 那么假设从某一笔交易开始不断往前追溯, 它所涉及的输入和输出到底是谁先存在呢?换个说法, 这是个鸡和蛋谁先谁后的问题, 是先有蛋还是先有鸡呢?

先有蛋

在比特币中, 是先有蛋, 然后才有鸡的. 输入引用输出的逻辑, 是经典的’蛋还是鸡’的问题: 输入先产生输出, 然后输出使得输入成为可能. 在比特币中, 最先有输出, 然后才有输入. 换而言之, 每一笔交易只有输出, 没有输入.
当miner挖出一个新的区块时, 它会向新的区块添加一个coinbase交易. coinbase交易是一种特殊的交易, 它不需要引用之前一笔交易的输出. 它’凭空’产生了币(也就是产生了新币), 这是miner获得挖出mining的奖励, 也可以理解为’发行新币’.
在区块链的最初, 也就是第一个块, 叫做创世区块. 正是这个创世区块, 产生了区块链最开始的输出.对于创世区块, 不需要引用之前交易的输出.因为在创世区块之前根本不存在交易, 也就是不存在交易输出.
来创建一个coinbase交易:

func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to %s", to)
	}

	txin := TXInput{[]byte{}, -1, data}
	txout := TXOutput{subsidy, to}
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
	tx.SetID()

	return &tx
}

coinbase交易只有一个输出, 没有输入. 在我们的实现中, 它表现为Txid为空, Vout等于-1.并且, 在当前实现中, coinbase交易也没有再ScriptSig中存储脚本, 而只是存储了一个任意的字符串data.

在比特币中, 第一笔coinbase交易包含了如下信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看

subsidy是挖出新区块的奖励金. 在比特币中, 实际并没有存储这个数字, 而是基于区块总数进行计算而得: 区块总数除以210000就是subsidy. 挖出创世区块的奖励是50BTC, 每挖出210000个区块后, 奖励减半. 在我们的实现中, 这个奖励值将会是一个常量(至少目前是).

将交易保存到区块链

从现在开始, 每个区块必须存储至少一笔交易. 如果没有交易, 也就不可能出新的块. 这意味着我们应该溢出Block的Data字段, 取而代之的是存储交易:

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}

NewBlock和NewGenesisBlock也必须做出相应的改变:

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	pow := NewProofOfWork(block)
	nonce, hash := pow.Run()

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}

接下来修改创建区块链的函数:

func CreateBlockchain(address string) *Blockchain {
	var tip []byte
	db, _ := bolt.Open(dbFile, 0600, nil)

	_ = db.Update(func(tx *bolt.Tx) error {
		cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
		genesis := NewGenesisBlock(cbtx)

		b, _ := tx.CreateBucket([]byte(blocksBucket))

		if b == nil {
			b, _ := tx.CreateBucket([]byte(blocksBucket))
			_ = b.Put(genesis.Hash, genesis.Serialize())
			_ = b.Put([]byte("l"), genesis.Hash)
			tip = genesis.Hash
		} else {
			tip = b.Get([]byte("l"))
		}
		return nil
	})
	bc := Blockchain{tip, db}

	return &bc
}

现在, 这个函数会接受一个地址作为参数, 这个地址将会被用来接收挖出创世区块的奖励.

工作量证明

工作量证明算法必须要将存储在区块里面的交易考虑进去, 从而保证区块链交易存储的一致性和可靠性. 所以, 我们必须修改ProofOfWork.prepareData方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(), // This line was changed
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

不像之前使用pow.block.Data, 现在我们使用pow.block.HashTransactions():

func (b *Block) HashTransactions() []byte {
	var txHashes [][]byte
	var txHash [32]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.ID)
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))
	return txHash[:]
}

通过哈希值提供数据的唯一表示, 这种做法我们已经不是第一次遇到了. 我们想要通过仅仅一个哈希值, 就可以识别一个块里面的所有交易. 为此, 先获得每笔交易的哈希值, 然后将它们关联起来, 最后获得一个连接后的组合哈希值.

比特币使用了一个更加复杂的技术: 它将一个区块里面包含的所有交易表示为一个Merkle tree, 然后在工作量证明系统中使用树的根哈希(root hash),这个方法能够让我们快速检索一个块里面是否包含了某笔交易, 即只需root hash而无需下载所有交易即可完成判断.

来检查目前为止是否正确:

╰─ ./blockchain_impl_in_go createblockchain -address Ivan                  ─╯ 
00000060b65eb7a78b68206835d12e06c8b00940da37b5c773c1d465a8a3a35f

Done!

很好, 我们已经获得了第一笔mining奖励, 但是, 我们要如何查看余额呢?

未花费的交易输出

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO), 未花费(unspent)指的是这个输出还没有被包含在任何交易的输入中, 或者说没有被任何输入引用. 在上面图示中, 未花费的输出是
在这里插入图片描述

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

当然了, 检查余额时, 我们并不需要知道整个区块链上所哟䣌UTXO, 只需要关注那些我们能解锁的那些UTXO(目前我们还没有实现密钥, 所以我们将会使用用户定义的地址来代替). 首先, 让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData
}

在这里, 我们只是将script字段与unlockingData进行了比较. 在后续文章我们基于私钥实现了地址以后, 会对这部分进行改进.
下一步, 找到包含未花费输出的交易, 这一步其实相当困难:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
	var upsentTXs []Transaction
	spentTXOs := make(map[string][]int)
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)
		Outputs:
			for outIdx, out := range tx.Vout {
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}
				if out.CanBeUnlockedWith(address) {
					upsentTXs = append(upsentTXs, *tx)
				}
			}
			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.CanUnlockOutputWith(address) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}

	return upsentTXs
}

由于交易被存储在区块里, 所以我们不得不检查区块链里的每一笔交易.从输出开始:

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, tx)
}

如果一个输出被一个地址锁定, 并且这个地址恰好是我们要找的地址, 那么这个输出就是我们想要的. 不过在获得它之前, 我们需要检查该输出是否已经被包含在一个交易的输出中, 也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
		if spentOut == outIdx {
			continue Outputs
		}
	}
}

我们跳过那些已经被包含在其他输入中的输出(这说明这个输出已经被花费, 无法再使用了). 检查完输出以后, 我们将给定地址所有能够解锁输出的输入聚合起来(这并不适用于coinbase交易, 因为它们不解锁输出)

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}

这个函数返回了一个交易列表, 里面包含了未花费输出.为了计算余额, 我们还需要一个函数将这些交易作为输入, 然后返回一个输出:

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)

       for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }

       return UTXOs
}

就是这么多了!现在我们来实现getbalance命令

func (cli *CLI) getBalance(address string) {
	bc := NewBlockchain(address)
	defer bc.db.Close()

	balance := 0
	UTXOs := bc.FindUTXO(address)

	for _, out := range UTXOs {
		balance += out.Value
	}

	fmt.Printf("Balance of '%s': %d\n", address, balance)
}

账户余额就是由账户地址锁定的所有未花费交易输出的综合.
在挖出创世区块后, 来检查一下我们的余额:

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 10

这就是我们的第一笔钱!

发送币

现在, 我们想要给其他人发送一些币. 为此, 我们需要创建一笔新的交易, 将它放到一个区块里, 然后挖出这个区块. 之前我们只实现了coinbase交易(这是一种特殊的交易), 现在我们需要一种通用的普通交易.

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	acc, validOutputs := bc.FindSpendableOutputs(from, amount)
	
	if acc < amount {
		log.Panic("ERROR: Not enough funds")
	}

	for txid, outs := range validOutputs {
		txID, _ := hex.DecodeString(txid)
		
		for _, out := range outs {
			input := TXInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}
	outputs = append(outputs, TXOutput{amount, to})
	if acc >amount {
		outputs = append(outputs, TXOutput{acc - amount, from})
	}
	tx := Transaction{nil, inputs, outputs}
	tx.SetID()
	
	return &tx
}

在创建新的输出前, 我们首先必须找到所有的未花费输出, 并且确保它们有足够的价值(value),这就是FindSpendableOutputs方法要做的事情. 随后, 对于每个找到的输出, 会创建一个引用该输出的输入. 接下来, 我们创建两个输出:

  1. 一个由接收者地址锁定. 这是给其他地址实际转移的币
  2. 一个由发送者地址锁定.这是一个找零.只有当未花费输出超过新交易所需时产生. 记住: 输出是不可再分的.

FindSpendableOutputs方法基于之前定义的FindUnspentTransactions方法:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0
Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)

		for outIdx, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

				if accumulated >= amount {
					break Work
				}
			}
		}
	}
	return accumulated, unspentOutputs
}

这个方法对所有未花费交易进行迭代, 并对它的值进行累加. 当累加值大于或等于我们想要传送的值时, 它就会停止并返回累加值, 同时返回的还有通过交易ID进行分组的输出索引. 我们只需取出足够支付的钱就够了.
现在我们可以修改Blockchain.MineBlock方法

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	_ = bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))

		return nil
	})

	newBlock := NewBlock(transactions, lastHash)

	_ = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		_ = b.Put(newBlock.Hash, newBlock.Serialize())
		_ = b.Put([]byte("l"), newBlock.Hash)
		bc.tip = newBlock.Hash

		return nil
	})
}

最后, 让我们实现send方法:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain(from)
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

发送币意味着创建新的交易, 并通过挖出新的区块的方式将交易打包到区块链中. 不过比特币并不是一连串立刻完成这些事情(虽然我们目前的实现时这么做的). 相反, 它会将所有新的交易放到一个内存池中(mempool),然后当miner准备挖出一个新区块时, 它从内存池中取出所有交易, 创建一个候选块. 只有当包含这些交易的区块被挖出来, 并添加到区块链以后, 里面的交易才开始确认.
让我们检查一下发送币是否能工作:

$ blockchain_go send -from Ivan -to Pedro -amount 6
000000655594c9b0c6c1034ec0236d91d2115bbd74ed008901ea81c29f231d7f

Success!

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 4

╰─ ./blockchain_impl_in_go getbalance -address Pedro                       ─╯ 
Balance of Pedro: 6

很好!现在, 让我们创建更多的交易, 确保从多个输出中发送币也正常工作:

╰─ ./blockchain_impl_in_go send -from Pedro -to Helen -amount 2            ─╯ 
0000003d3c12819d42b9c9a2968a803b651a775af1af262d51384d6c2577f8e1

Success!

╰─ ./blockchain_impl_in_go send -from Ivan -to Helen -amount 2             ─╯ 
00000050e41ef1982c2aab6ad43d748b7f65c720a34a6fb9f144960d911e4711

Success!

现在, Helen的币被锁定在了两个输出中: 一个来自Pedro, 一个来自Lvan.让我们把它们发送给其他人:

╰─ ./blockchain_impl_in_go send -from Helen -to Rachel -amount 3           ─╯ 
000000417eabcad236c9d42c5c33d72c5e63a293c2b5491390f362d1d3fcdb89

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3

看起来没问题!现在, 来测试一些失败的情况:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

总结

虽然不容易, 但是现在终于实现交易了!不过, 我们依然缺少了一些像比特币那样的一些关键特性:

  1. 地址(address). 我们现在还没有基于私钥(private key)的真实地址.
  2. 奖励(reward). 现在mining时肯定无法盈利的!
  3. UTXO集. 获取余额需要扫描整个区块链, 而当区块非常多时, 这么做就会花费很长时间. 并且, 如果我们想要验证后续交易, 也需要花费很长时间. 而UTXO集就是为了解决这些问题, 加快交易相关的操作.
  4. 内存池(mempool). 在交易被打包到区块之前, 这些交易被存储在内存池里面. 在我们目前的实现中,一个块仅仅包含一笔交易, 这是相当低效的.

link

Full source codes
Transaction
Merkle tree
Coinbase

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564609.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

让你在Windows打开Sketch格式再也不愁

Sketch是Macos的专用矢量绘图应用。在Sketch软件中&#xff0c;ios开发者可以轻松设计图层面板等图层的常用操作&#xff0c;广泛应用于产品的交互设计和UI设计&#xff0c;帮助很多设计师创作出很多优秀的作品。然而&#xff0c;Sketch只服务于Macos系统&#xff0c;这使得许多…

Laravel框架06:文件、迁移填充、会话、缓存

Laravel框架06&#xff1a;文件、迁移填充、会话、缓存 一、文件上传1. 文件上传表单2. 上传业务处理3. 全部代码 二、数据表的迁移与填充1. 迁移文件① 创建迁移文件② 编写迁移文件③ 执行迁移文件④ 回滚迁移文件 2. 填充&#xff08;种子&#xff09;文件① 创建填充文件②…

C++常用的支持中文的GUI库Qt 6之三: Qt 6的项目的发布

C常用的支持中文的GUI库Qt 6之三&#xff1a; Qt 6的项目的发布 本文接着上一篇“C常用的支持中文的GUI库Qt 6之二&#xff1a;项目的结构、资源文件的使用” https://blog.csdn.net/cnds123/article/details/130741807介绍&#xff0c;并使用其中的例子。 程序代码能正确编译…

【STL】list的使用

系列文章 学习C途中自然绕不过STL&#xff0c;在这个系列文章之中 我们讲了string的使用和string的模拟实现&#xff0c;以及vector的使用、vector的模拟实现。 感兴趣的可以翻翻看。 目录 系列文章 前言 默认成员函数 构造函数 拷贝构造 赋值重载 迭代器 容量查询 …

人人都能看懂的Spring源码解析,Spring声明式事务关于传播特性、事务挂起与恢复的处理

人人都能看懂的Spring源码解析&#xff0c;Spring声明式事务关于传播特性、事务挂起与恢复的处理 原理解析AbstractPlatformTransactionManager事务传播特性事务挂起与恢复通过DataSourceTransactionManager看事务挂起和恢复的具体实现 代码走读总结 往期文章&#xff1a; 人人…

LRU Cache

前言 哈喽&#xff0c;各位小伙伴大家好&#xff0c;本章内容为大家介绍计算机当中为了提高数据相互传输时的效率而引进的一种重要设计结构叫做LRU Cache,下面将为大家详细介绍什么是LRU Cache,以及它是如何是实现的&#xff0c;如何提升效率的。 1.什么是LRU Cache? LRU是L…

卷起来了?2023这三个项目直接让你原地起飞!

理论自学谁不会&#xff0c;理论知识跟实战项目实践相结合才是大问题&#xff1f; 还在发愁没有项目练手&#xff1f;还在发愁简历中的项目生搬硬凑&#xff1f;还在担心自己没实操过项目被面试官直接K.O? 这三个实战项目让你快人一步&#xff0c;总有一个适合你的&#xff…

数慧时空20年磨一剑:推出智能遥感云平台DIEY,自然资源多模态大模型“长城”,为地理信息产业提速

作者 | 伍杏玲 出品 | CSDN 据中国地理信息产业发展报告公布的数据&#xff0c;截至2020年末&#xff0c;行业从业单位13.8万家&#xff0c;从业人数336.6万&#xff0c;到2021年末&#xff0c;从业单位增加到16.4万家&#xff0c;从业人数增加到398万&#xff0c;产业规模越…

软件测试的未来?为什么越来越多的公司选择模糊测试

背景&#xff1a;近年来&#xff0c;随着信息技术的发展&#xff0c;各种新型自动化测试技术如雨后春笋般出现。其中&#xff0c;模糊测试&#xff08;fuzz testing&#xff09;技术开始受到行业关注&#xff0c;它尤其适用于发现未知的、隐蔽性较强的底层缺陷。这里&#xff0…

Eclipse MAT分析内存案例

前言 本文记录一次使用Eclipse MAT排查内存问题的案例&#xff0c;缘由是线上某服务OOM&#xff0c;排查得知jvm old区占满&#xff0c;但是gc了还是无法释放 实战 首先在线上服务器排查发现某应用占用了大量的内存&#xff0c;由一个ConcurrentHashMap对象造成的&#xff0…

【ArcGIS Pro二次开发】(31):ArcGIS Pro中的多线程

ArcGIS Pro与旧的ArcGIS桌面应用程序的显著不同之处在于&#xff0c;它采用多线程架构&#xff0c;可以有效的发挥多核CPU的优势。这使得二次开发工具的性能变得更好&#xff0c;但也对开发工作带来了更多的难点和挑战。 一、多线程需要注意的问题 一般情况下&#xff0c;为了…

尚硅谷MyBatis-Plus笔记001【简介、入门案例、基本CRUD】

视频地址&#xff1a;【尚硅谷】MyBatisPlus教程&#xff08;一套玩转mybatis-plus&#xff09;_哔哩哔哩_bilibili 尚硅谷MyBatis-Plus笔记01【简介、入门案例、基本CRUD】 尚硅谷MyBatis-Plus笔记02【】 尚硅谷MyBatis-Plus笔记03【】 尚硅谷MyBatis-Plus笔记04【】 尚硅谷…

非暴力沟通--日常沟通的技巧与实践

这篇文章是我在公司团队内部做的分享的演讲稿 开场白 大家好&#xff0c;我今天要分享的主题是非暴力沟通–日常沟通的技巧与实践。 介绍非暴力沟通这本书 分享这个主题的原因是我最近看了一本书&#xff0c;叫做《非暴力沟通》&#xff0c;这本书是美国一个叫做马歇尔卢森堡…

MFC CListCtrl 显示图片

MFC CListCtrl 显示图片 MFC CListCtrl 显示图片PreCreateWindow中设置风格没有起作用在OnCreate中设置CListCtrl的风格最合适在OnInitialUpdate中添加数据最合适需要设置CImageList&#xff0c;资源是我自己搞的一个图片资源ps:参考链接 MFC CListCtrl 显示图片 在使用MFC的C…

Codeforces Round 764 (Div. 3)

比赛链接 Codeforces Round 764 A. Plus One on the SubsetB. Make APC. Division by Two and PermutationD. Palindromes ColoringE. Masha-forgetful A. Plus One on the Subset Example input 3 6 3 4 2 4 1 2 3 1000 1002 998 2 12 11output 3 4 1题意&#xff1a; 你可…

怎么学习机械学习相关的技术?

学习机器学习相关技术的过程可以分为以下几个步骤&#xff1a; 掌握基本数学和统计知识&#xff1a; 机器学习建立在数学和统计学的基础上&#xff0c;了解线性代数、概率论、统计学等基本概念和方法对于理解机器学习算法至关重要。 学习编程和数据处理&#xff1a; 掌握一门…

kafka基础介绍

目录 前言&#xff1a; 一:kafka架构 1.kafka基础架构 2、kafka多副本架构 二、kafka基础概念 1、produce 2. Consumer 3、Broker ​ 4、Topic 5、Partition 6、Replicas 7、Offset 8、 AR 9、 ISR 10、OSR 11、HW 12、LEO 13、Lag 三、kafka特性 四、kafka…

总结加载Shellcode的各种方式

1.内联汇编加载 使用内联汇编只能加载32位程序的ShellCode&#xff0c;因为64位程序不支持写内联汇编 #pragma comment(linker, "/section:.data,RWE") //将data段的内存设置成可读可写可执行 #include <Windows.h>//ShellCode部分 unsigned char buf[] &qu…

FreeRTOS-任务通知详解

✅作者简介&#xff1a;嵌入式入坑者&#xff0c;与大家一起加油&#xff0c;希望文章能够帮助各位&#xff01;&#xff01;&#xff01;&#xff01; &#x1f4c3;个人主页&#xff1a;rivencode的个人主页 &#x1f525;系列专栏&#xff1a;玩转FreeRTOS &#x1f4ac;保持…

Spring Security入门

1. Spring Security 简介 Spring Security 是一个高度可定制的身份验证和访问控制框架&#xff0c;它基于 Spring 框架&#xff0c;并可与 Spring 全家桶无缝集成。该框架可以精确控制用户对应用程序的访问&#xff0c;控制用户的角色和权限等。 Spring Security 最早是由 Be…