数据库控制并发字段的设计与想法
数据库控制并发字段的设计与想法
0. 原场景
最近在做一些增删改查,发现了一点我之前从未想过的场景,在高并发的场景下,假如多个用户对同一条信息进行修改,那么这个信息该如何去update呢?仅仅根据id条件去更新,它会发生什么?
1. 场景demo
这里使用Spring Boot + Mybatis + Thymeleaf做了一个demo。
界面,如下图所示:
html核心代码如下:
<form th:action="@{/person/update}" method="post">
<input type="hidden" id="id" name="id" th:value="${person.id}">
<div class="mb-3 row">
<label for="name" class="col-sm-2 col-form-label">姓名</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="name" name="name" th:value="${person.name}">
</div>
</div>
<div class="mb-3 row">
<label for="money" class="col-sm-2 col-form-label">金钱</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="money" name="money" th:value="${person.money}">
</div>
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
数据库设计很简单,如下图:
Controller层如下:
为了方便看到 是否更新成功,判断了sql执行后的行数
@Controller
public class PersonController {
@Autowired(required = false)
PersonService personService;
@RequestMapping(path = "/person/{id}", method = RequestMethod.GET)
public String getPerson(@PathVariable(name = "id")int id, Model model){
Person person = personService.selectById(id);
model.addAttribute("person", person);
return "person";
}
@RequestMapping(path = "/person/update", method = RequestMethod.POST)
public String updatePerson(Person person, Model model){
int r = personService.update(person);
if (r == 1) {
System.out.println("更新成功");
} else {
System.out.println("更新失败");
}
return "redirect:/person/"+person.getId();
}
}
Service层如下:
@Service
public class PersonService {
@Autowired(required = false)
PersonMapper personMapper;
public Person selectById(int id){
return personMapper.selectById(id);
}
public int update(Person p){
return personMapper.update(p);
}
}
Mapper层如下:
@Mapper
public interface PersonMapper {
Person selectById(int id);
int update(Person p);
}
Mapper.xml如下:
<select id="selectById" resultType="Person">
select id, name, money
from person
where id = #{id}
</select>
<update id="update" parameterType="Person" keyProperty="id">
update person set name=#{name}, money = #{money} where id = #{id}
</update>
最后在配置文件中,将mapper调整日志级别为debug,方便看到sql执行情况
2. demo测试
我们运行项目,同时打开两个页面,模仿有多个用户同时进行修改操作,如下图所示:
然后我们非常快速的点击两个页面的修改,看看会发生什么
在测试的时候我先点了左边的,后点了右边的,最后变成了如下样子:
控制台日志如下:
2022-06-12 12:58:49.844 DEBUG 11304 --- [nio-8888-exec-5] c.x.example.dao.PersonMapper.update : ==> Preparing: update person set name=?, money = ? where id = ?
2022-06-12 12:58:49.846 DEBUG 11304 --- [nio-8888-exec-5] c.x.example.dao.PersonMapper.update : ==> Parameters: 小明(String), 9999(Integer), 1(Integer)
2022-06-12 12:58:50.269 DEBUG 11304 --- [nio-8888-exec-6] c.x.example.dao.PersonMapper.update : ==> Preparing: update person set name=?, money = ? where id = ?
2022-06-12 12:58:50.276 DEBUG 11304 --- [nio-8888-exec-6] c.x.example.dao.PersonMapper.update : ==> Parameters: 小明(String), 100(Integer), 1(Integer)
2022-06-12 12:58:50.298 DEBUG 11304 --- [nio-8888-exec-5] c.x.example.dao.PersonMapper.update : <== Updates: 1
更新成功
2022-06-12 12:58:50.328 DEBUG 11304 --- [nio-8888-exec-6] c.x.example.dao.PersonMapper.update : <== Updates: 1
更新成功
2022-06-12 12:58:50.441 DEBUG 11304 --- [nio-8888-exec-7] c.x.example.dao.PersonMapper.selectById : ==> Preparing: select id, name, money from person where id = ?
2022-06-12 12:58:50.442 DEBUG 11304 --- [nio-8888-exec-7] c.x.example.dao.PersonMapper.selectById : ==> Parameters: 1(Integer)
2022-06-12 12:58:50.445 DEBUG 11304 --- [nio-8888-exec-7] c.x.example.dao.PersonMapper.selectById : <== Total: 1
2022-06-12 12:58:50.500 DEBUG 11304 --- [nio-8888-exec-8] c.x.example.dao.PersonMapper.selectById : ==> Preparing: select id, name, money from person where id = ?
2022-06-12 12:58:50.501 DEBUG 11304 --- [nio-8888-exec-8] c.x.example.dao.PersonMapper.selectById : ==> Parameters: 1(Integer)
2022-06-12 12:58:50.511 DEBUG 11304 --- [nio-8888-exec-8] c.x.example.dao.PersonMapper.selectById : <== Total: 1
我们可以看到,两次更新都成功了,但是对于左边和右边两个用户来讲,显然左边并不是用户想要的结果。
因此,允许我粗略的进行表达,我们需要控制并发,在同一时刻只能有一个更新成功,其他的都更新失败。
3. 设计改进
因此基于以上考虑,怎么能保证同一时刻只能有一个更新成功,其他的欧更新失败呢?对此我遇见了一种解决方案,从字段的角度上来控制并发场景。
给数据库做如下改造,添加一个coluuid
字段,代表这一条记录的uuid,更新时根据id 和coluuid去查找记录,并且更新时设置一个新的uuid。
页面依旧如此,但是form里面添加了一个type为hidden,name为coluuid的input:
<input type="hidden" id="colUuid" name="colUuid" th:value="${person.colUuid}">
关于person的entity实体我们要添加两个字段,如下所示:
Controller层同之前的样子,Service层我们要做一些更新,如下所示:
对传入的person对象设置一个新的coluuid
Mapper层同之前的
Mapper.xml如下:
核心就在于在更新的时候是根据id 和 coluuid 去更新,并且设置一个新的coluuid
4. demo1测试
依旧运行项目,打开两个窗口,模仿多用户同时进行修改:
然后我们非常快速的点击A页面的修改以及B页面的修改,之后我们发现如下所示:
这次和demo不同的是,最先点击修改的用户成功了,之后的修改并未成功,我们看下日志:
2022-06-12 13:15:20.356 DEBUG 9024 --- [nio-8889-exec-5] c.x.example.dao.PersonMapper.update : ==> Preparing: update person2 set name=?, money = ?, coluuid = ? where id = ? and coluuid = ?
2022-06-12 13:15:20.365 DEBUG 9024 --- [nio-8889-exec-5] c.x.example.dao.PersonMapper.update : ==> Parameters: 张三(String), 100(Integer), 3f9b855f-a507-4f6b-8da1-d18b96d67141(String), 1(Integer), 985a3700-5c0f-4531-a599-143c8a9b5378(String)
2022-06-12 13:15:21.074 DEBUG 9024 --- [nio-8889-exec-6] c.x.example.dao.PersonMapper.update : ==> Preparing: update person2 set name=?, money = ?, coluuid = ? where id = ? and coluuid = ?
2022-06-12 13:15:21.082 DEBUG 9024 --- [nio-8889-exec-6] c.x.example.dao.PersonMapper.update : ==> Parameters: 张三(String), 99(Integer), c0e6fabf-c36b-47d8-a412-cc10ae3d0242(String), 1(Integer), 985a3700-5c0f-4531-a599-143c8a9b5378(String)
2022-06-12 13:15:21.085 DEBUG 9024 --- [nio-8889-exec-6] c.x.example.dao.PersonMapper.update : <== Updates: 0
2022-06-12 13:15:21.085 DEBUG 9024 --- [nio-8889-exec-5] c.x.example.dao.PersonMapper.update : <== Updates: 1
更新失败
更新成功
2022-06-12 13:15:21.161 DEBUG 9024 --- [nio-8889-exec-7] c.x.example.dao.PersonMapper.selectById : ==> Preparing: select id, name, money, coluuid from person2 where id = ?
2022-06-12 13:15:21.163 DEBUG 9024 --- [nio-8889-exec-7] c.x.example.dao.PersonMapper.selectById : ==> Parameters: 1(Integer)
2022-06-12 13:15:21.165 DEBUG 9024 --- [nio-8889-exec-7] c.x.example.dao.PersonMapper.selectById : <== Total: 1
2022-06-12 13:15:21.257 DEBUG 9024 --- [nio-8889-exec-8] c.x.example.dao.PersonMapper.selectById : ==> Preparing: select id, name, money, coluuid from person2 where id = ?
2022-06-12 13:15:21.260 DEBUG 9024 --- [nio-8889-exec-8] c.x.example.dao.PersonMapper.selectById : ==> Parameters: 1(Integer)
2022-06-12 13:15:21.263 DEBUG 9024 --- [nio-8889-exec-8] c.x.example.dao.PersonMapper.selectById : <== Total: 1
第一次的更新成功,第二次的更新并未成功,我们把SQL拿出来单独研究下:
Preparing: update person2 set name=?, money = ?, coluuid = ? where id = ? and coluuid = ?
Parameters: 张三(String), 100(Integer), 3f9b855f-a507-4f6b-8da1-d18b96d67141(String), 1(Integer), 985a3700-5c0f-4531-a599-143c8a9b5378(String)
Preparing: update person2 set name=?, money = ?, coluuid = ? where id = ? and coluuid = ?
Parameters: 张三(String), 99(Integer), c0e6fabf-c36b-47d8-a412-cc10ae3d0242(String), 1(Integer), 985a3700-5c0f-4531-a599-143c8a9b5378(String)
Updates: 0
Updates: 1
更新失败
更新成功
两次更新都是按照同一个id 和 coluuid去更新的,因为我们先开始查询用户的时候就已经将这个字段信息写死在html文件中的form表单中了,
但是第一次更新的话肯定会成功,并且设置一个新的coluuid,第二次的话就会失败,因为根据原有的id 和 coluuid 已经查询不到那条记录了。
5. 尾记
以上就是我的对数据库控制并发字段的一些浅显的理解,实际上数据库中底层原理的控制并发并不是如我写的一样,而是遵循MVCC多版本并发控制。具体原理有待深入学习,欢迎大家留言讨论。