数据库控制并发字段的设计与想法

数据库控制并发字段的设计与想法

0. 原场景

最近在做一些增删改查,发现了一点我之前从未想过的场景,在高并发的场景下,假如多个用户对同一条信息进行修改,那么这个信息该如何去update呢?仅仅根据id条件去更新,它会发生什么?

1. 场景demo

这里使用Spring Boot + Mybatis + Thymeleaf做了一个demo。

界面,如下图所示:

image

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>

数据库设计很简单,如下图:

image

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测试

我们运行项目,同时打开两个页面,模仿有多个用户同时进行修改操作,如下图所示:

image

然后我们非常快速的点击两个页面的修改,看看会发生什么

在测试的时候我先点了左边的,后点了右边的,最后变成了如下样子:

image

控制台日志如下:

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。

image

页面依旧如此,但是form里面添加了一个type为hidden,name为coluuid的input:

<input type="hidden" id="colUuid" name="colUuid" th:value="${person.colUuid}">

image

关于person的entity实体我们要添加两个字段,如下所示:

image

Controller层同之前的样子,Service层我们要做一些更新,如下所示:

image

对传入的person对象设置一个新的coluuid

Mapper层同之前的

Mapper.xml如下:

核心就在于在更新的时候是根据id 和 coluuid 去更新,并且设置一个新的coluuid

image-20220612131348785

4. demo1测试

依旧运行项目,打开两个窗口,模仿多用户同时进行修改:

image

然后我们非常快速的点击A页面的修改以及B页面的修改,之后我们发现如下所示:

image

这次和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多版本并发控制。具体原理有待深入学习,欢迎大家留言讨论。

posted @ 2022-06-12 13:26  熊子q  阅读(101)  评论(0编辑  收藏  举报