您可能忽略了REST API中的PUT方法

作者:API传播员 · 2026-01-17 · 阅读时间:6分钟
本文探讨了在REST API中实现PUT方法时常见的数据竞争问题,通过Golang示例展示了乐观锁定解决方案,包括添加版本列和更新逻辑改进,帮助开发者避免并发更新冲突。

您可能忽略了 REST API 中的 PUT 方法

构建 REST API 时,无论是您的第一个项目还是第百个项目,或者您正在学习相关教程,您可能会涉及到为端点实现 PUT HTTP 方法。为了更好地理解如何实现 PUT 方法的基本工作流程,我们将通过一个示例代码片段进行演示。本文中使用的是 Golang,但您可以使用自己熟悉的编程语言。以下是一个名为 Pokemons 的表的模式:

CREATE TABLE pokemons(
 id bigserial PRIMARY KEY,
 created_at TIMESTAMP(0) with time zone NOT NULL DEFAULT NOW(),
 name TEXT NOT NULL,
 region TEXT NOT NULL,
 atk INTEGER NOT NULL,
 def INTEGER NOT NULL
);

使用 GET 方法检索数据

在实现更新逻辑之前,我们需要先通过 GET 方法检索用户想要更新的记录。以下是一个示例代码:

return &pokemon, nil
}

func (m PokemonModel) Get(id int64) (*Pokemon, error) {

if id < 1 {
return nil, ErrRecordNotFound
}

query := `
SELECT id, created_at, name, region, atk, def
FROM pokemons
WHERE id=$1
`

var pokemon Pokemon

// 设置超时上下文,防止查询时间过长
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

// 查询数据库并将结果存储到 pokemon 结构体中
err := m.DB.QueryRowContext(ctx, query, id).Scan(&pokemon.ID, &pokemon.CreatedAt,
&pokemon.Name, &pokemon.Region, &pokemon.Atk, &pokemon.Def)

if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}

return &pokemon, nil
}

实现更新方法

在获取到记录后,我们可以通过以下代码实现更新操作:

func (m PokemonModel) Update(pokemon *Pokemon) error {
 // 更新 SQL 查询
 query := `
 UPDATE pokemons
 SET name=$1, region=$2, atk=$3, def=$4
 WHERE id=$5
 RETURNING id
 `

 // 占位符参数的值
 args := []interface{}{pokemon.Name, pokemon.Region, pokemon.Atk,
  pokemon.Def, pokemon.ID} // 设置超时上下文
 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 defer cancel() // 执行更新查询并返回结果
 return m.DB.QueryRowContext(ctx, query, args...).Scan(&pokemon.ID)
}

到这里,您可能会认为更新逻辑已经完成。然而,如果我告诉您,这种实现方式可能会导致 数据竞争 问题,您会怎么想?


数据竞争问题的产生

数据竞争是指在多个线程同时访问共享资源时,由于操作顺序的不确定性而导致的冲突问题。在我们的示例中,Pokemons 表中的记录是一个共享资源,可能会被多个线程同时访问。

例如,Alice 和 Bob 同时对 id 为 376 的记录发起 GET 请求,并分别获取到相同的记录。当他们几乎同时发送 PUT 请求来更新记录时,就会发生数据竞争。

  • Alice 想将 atk 字段更新为 330。
  • Bob 想将 def 字段更新为 400。

由于请求处理的顺序不确定,最终可能只会保留 Alice 的更新,而 Bob 的更新被覆盖。这种情况显然是不理想的。


解决数据竞争的方法

为了解决数据竞争问题,我们可以采用以下两种主要方法:

  1. 悲观锁定:在操作资源时锁定记录,直到操作完成。
  2. 乐观锁定:通过版本控制机制检测并避免冲突。

在本文中,我们将使用 乐观锁定 来解决数据竞争问题。


在表中添加版本列

使用乐观锁定的第一步是为表添加一个 version 列,用于跟踪记录的版本号。初始版本号默认为 1:

ALTER TABLE pokemons
ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

在查询中包含版本字段

接下来,我们需要在 GET 方法的查询中包含 version 字段:

query := `
 SELECT id, created_at, name, region, atk, def, version
 FROM pokemons
 WHERE id=$1
`
// 将 version 字段存储到结构体中
err := m.DB.QueryRowContext(ctx, query, id).Scan(&pokemon.ID, &pokemon.CreatedAt,
 &pokemon.Name, &pokemon.Region, &pokemon.Atk, &pokemon.Def, &pokemon.Version)

更新方法的改进

在更新记录时,我们需要确保版本号未被其他请求修改,同时将版本号递增 1。以下是改进后的更新方法:

// 自定义错误,用于通知数据竞争
var ErrEditConflict = errors.new("edit conflict")

func (m PokemonModel) Update(pokemon *Pokemon) error { query := `
 UPDATE pokemons
 SET name=$1, region=$2, atk=$3, def=$4, version=version+1
 WHERE id=$5 AND version=$6
 RETURNING version
 ` args := []interface{}{pokemon.Name, pokemon.Region, pokemon.Atk,
  pokemon.Def, pokemon.ID, pokemon.Version} ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 defer cancel() err := m.DB.QueryRowContext(ctx, query, args...).Scan(&pokemon.Version) if err != nil {
  switch {
  case errors.Is(err, sql.ErrNoRows):
   return ErrEditConflict
  default:
   return err
  }
 }
 return nil
}

通过在更新条件中加入版本号检查,我们可以确保只有在版本号未被修改的情况下才能进行更新操作。


后续操作

处理数据竞争的一个可能后续操作是使用自定义错误(如 ErrEditConflict),将冲突事件发送到事件队列中,并在后台工作器中重试失败的查询。无论您选择哪种方式,重要的是采取措施解决数据竞争问题。

尽管本文中的示例较为简单,但类似的错误在某些场景下可能会导致严重后果,例如更新账户余额时。因此,理解和解决这些问题是至关重要的。

希望本文能帮助您更好地理解在 REST API 中实现 PUT 方法时可能遇到的问题,并为您提供解决方案的思路。

原文链接: https://medium.com/@k1nho/you-might-be-overlooking-the-put-method-in-your-rest-api-156cd86c852d