问题

用户和管理员同时操作同一记录的不同字段,如果做并发控制?

回答
当用户和管理员同时操作同一条记录的不同字段时,这确实是一个常见的并发控制挑战。我们既要保证数据的准确性,又要避免相互干扰,影响操作体验。下面我就来详细聊聊这个问题,尽量用一种更贴近实际场景的方式来讲述。

想象一下,我们有一个客户信息管理系统。一条记录里包含了很多字段,比如客户姓名、联系电话、地址、备注信息等等。现在,一个客服人员(我们称他为“用户”)正在修改客户的联系电话,而与此同时,一个负责审核的管理员(我们称他为“管理员”)也在编辑同一客户的地址信息。

如果不对这种情况做任何控制,可能会发生什么?

最糟糕的情况:数据丢失

假设系统没有并发控制。

1. 客服人员读取了客户A的信息,包括姓名“张三”,电话“13800138000”,地址“XX市XX区”。
2. 管理员也读取了客户A的信息,同样的姓名“张三”,电话“13800138000”,地址“XX市XX区”。
3. 客服人员修改了电话,将其变为“13900139000”,然后保存。此时,数据库中的客户A信息变成了:姓名“张三”,电话“13900139000”,地址“XX市XX区”。
4. 管理员修改了地址,将其变为“YY市YY区”,然后保存。这时,由于管理员读取的是最原始的数据,他的保存操作会覆盖掉客服人员刚刚修改的电话信息。最终客户A的信息变成了:姓名“张三”,电话“13800138000”,地址“YY市YY区”。

你看,客服人员辛苦修改的电话号码就这么凭空消失了。这就是典型的“丢失更新”问题。

如何解决这个问题?

核心在于,我们需要一种机制来知道“谁在修改什么,以及我的修改是否基于最新版本的数据”。

1. 乐观并发控制 (Optimistic Concurrency Control OCC)

这是最常见,也最推荐的方式。它的核心思想是:“并发冲突的发生概率不高,所以我们先允许操作,然后在提交的时候再检查有没有冲突。”

怎么实现呢?

版本号(Version Number):给每一条记录增加一个版本号字段。每次数据被修改后,版本号都会自增。

当用户或管理员读取一条记录时,他们会同时读取到这条记录的最新版本号。
当用户或管理员尝试更新记录时,他们会提交新的数据,以及他们读取时看到的那个版本号。
数据库在执行更新时,会检查数据库里这条记录的当前版本号是否与提交时带来的版本号相等。
如果相等,说明在读取和写入之间,这条记录没有被别人修改过,更新成功,版本号自增。
如果不相等,说明在读取之后,这条记录已经被别人修改了。这时,数据库会拒绝这次更新,并返回一个错误(比如“数据已被修改,请刷新后重试”)。

举个例子:
1. 客户A的记录,初始版本号是1:{姓名: “张三”, 电话: “13800138000”, 地址: “XX市XX区”, 版本号: 1}
2. 客服人员读取,得到 {姓名: “张三”, 电话: “13800138000”, 地址: “XX市XX区”, 版本号: 1}
3. 管理员也读取,得到 {姓名: “张三”, 电话: “13800138000”, 地址: “XX市XX区”, 版本号: 1}
4. 客服人员修改电话,准备提交:{姓名: “张三”, 电话: “13900139000”, 地址: “XX市XX区”, 版本号: 1}
5. 管理员修改地址,准备提交:{姓名: “张三”, 电话: “13800138000”, 地址: “YY市YY区”, 版本号: 1}
6. 客服人员先提交。数据库检查:当前版本号(1) == 提交的版本号(1),匹配!更新成功,版本号自增为2。数据库变成:{姓名: “张三”, 电话: “13900139000”, 地址: “XX市XX区”, 版本号: 2}
7. 管理员提交。数据库检查:当前版本号(2) != 提交的版本号(1),不匹配!拒绝更新,并返回错误。
8. 管理员收到错误后,可以选择重新读取(刷新)数据,得到最新的版本号2,然后在他看到的基础上进行修改(比如电话已经改了,他需要先确认是保留哪个电话),再重新提交。

时间戳(Timestamp):原理和版本号类似,但使用一个时间戳字段来记录最后修改的时间。每次修改,时间戳更新。检查时比较当前时间戳和读取时的时间戳。虽然原理相似,但在高并发下,版本号通常比时间戳更可靠,因为时间戳可能存在时钟漂移或精度问题。

在“同时操作不同字段”场景下的乐观并发控制:

乐观并发控制天然适合“同时操作不同字段”的情况。

为什么? 因为乐观锁只关心整个记录的版本号。它不关心是谁修改了哪个字段,只关心“我读取的版本号”和“数据库当前的版本号”是否一致。
好处:
高性能: 在没有冲突的时候,几乎没有额外的锁开销。
避免死锁: 因为它不是真的加锁,所以不存在死锁问题。
用户体验: 当冲突不常发生时,用户基本感受不到延迟。即使发生冲突,提示用户刷新也是一种可接受的交互。

实现上的考虑(更深入一点):

前端实现:
当页面加载时,除了显示数据,还要将该数据的版本号(或时间戳)也一并存储在一个隐藏的字段或Vue/React组件的state里。
用户或管理员进行修改后,在提交时,除了要保存的字段值,还要带上那个隐藏的版本号。
如果提交失败(收到“数据已被修改”的提示),前端应该提示用户:“数据已更新,请刷新后重新操作。” 刷新后,重新读取数据,并更新界面和版本号,用户才能继续操作。

后端实现:
后台的API接收到更新请求时,需要从请求中解析出要更新的字段、新的字段值,以及那个伴随的“版本号”。
在执行SQL UPDATE语句时,会加入一个`WHERE`条件:`WHERE id = ? AND version = ?`。
数据库执行该SQL后,会返回一个“影响的行数”。
如果影响的行数是1,说明更新成功,版本号自然会被数据库(或应用逻辑)自增。
如果影响的行数是0,说明没有找到符合`id`和`version`条件的记录,意味着版本号不匹配,就发生了并发冲突。此时,后台应该返回一个相应的错误码或消息给前端。

2. 悲观并发控制 (Pessimistic Concurrency Control PESSIMISTIC)

相比乐观锁,悲观锁的思想是:“并发冲突的发生概率很高,所以我们在操作数据之前,先把它‘锁’住,不让别人动,等我操作完了再解锁。”

如何实现?
行级锁(RowLevel Locking):数据库系统提供。当一个用户尝试修改某条记录时,它会向数据库申请对这条记录加锁。一旦加锁成功,其他用户就无法对这条记录进行读写操作,直到锁被释放。
读锁(Shared Lock):允许多个用户同时读取,但不允许写入。
写锁(Exclusive Lock):只允许一个用户进行写操作,其他读写都被阻塞。

在“同时操作不同字段”场景下的悲观并发控制:
问题: 如果客服人员和管理员都对同一条记录申请了“写锁”,那么谁先拿到锁,另一个人就只能等待。即使他们操作的是不同的字段,也可能因为锁的粒度太粗,导致一方等待另一方,影响效率。
更精细的锁? 理论上可以设计字段级别的锁,但这会大大增加数据库的复杂性和锁的管理成本,通常不实用。
场景限制: 悲观锁更适合那些冲突发生概率非常高,或者操作流程必须保证原子性(即必须一次性完成,不允许被打断)的场景。例如,金融交易中的账户余额扣除,必须确保万无一失。

为什么通常不首选悲观锁来解决“不同字段修改”的并发问题?
性能影响: 频繁加锁和解锁会带来显著的性能开销。
死锁风险: 当多个用户之间相互等待对方释放锁时,就可能发生死锁,需要复杂的机制来检测和解决。
阻塞: 锁会阻塞其他用户,降低系统的吞吐量和响应速度。
复杂性: 实现和管理锁的逻辑会更复杂。

总结一下:

对于“用户和管理员同时操作同一记录的不同字段”这种场景,乐观并发控制(使用版本号)是更优的选择。

它允许用户和管理员并行地编辑,直到最后提交时才进行一次高效的冲突检测。如果发生冲突,系统会及时通知用户,并提供一个简单的刷新和重试机制,既保证了数据的准确性,又最大程度地减少了性能损耗和用户等待。

想象一下,你正在网上填写一份很长的注册表单,你填写了一半,突然提示你“表单已过期,请重新登录”。那种感觉非常糟糕。乐观锁的目的是尽量避免这种情况,在你填写完毕准备提交时,才告诉你“有人在你填写的时候修改了关键信息,请你确认最新的内容”。这是一种更平滑、更友好的用户体验。

网友意见

user avatar

很简单,出现这种问题是因为你没有版本的概念,属于需求分析不彻底,业务概念不清晰。

管理员是针对内容的版本进行审核的,而不是针对什么记录


理清了业务需求,解决方案是明摆着的……

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有