用最简单的springboot+mybatis解释for update的使用场景

19 篇文章 1 订阅
4 篇文章 0 订阅

正经学徒,佛系记录,不搞事情

本文的主角是mysql InnoDB的写锁,即排他锁(for update)

使用他最好的方式就是理解他:

  1. 排他锁不能与其他锁共存
  2. 一个事务获取了某行的排他锁,其他事务就不能再获取该行的锁
  3. 获取排他锁的当前事务内可以对数据进行读取和修改
  4. 不开启事务,FOR UPDATE 不会锁数据
  5. FOR UPDATE 是写锁操作不会锁住
  6. FOR UPDATE 即可能是行锁也可能是表锁

关于是行还是表锁,参考 https://www.jianshu.com/p/682dc24d7f19

假设有个表单products ,里面有id跟name二个栏位,id是主键。
例1: (明确指定主键,并且有此笔资料,row lock)
SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;
例2: (明确指定主键,若查无此笔资料,无lock)
SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;
例2: (无主键,table lock)
SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;
例3: (主键不明确,table lock)
SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;
例4: (主键不明确,table lock)
SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;

带着上面的总结,开始试验:

建立一张最基本的表,数据库增加数据id=1,score=0

CREATE TABLE `score` (
  `id` int(6) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `score` int(6) DEFAULT NULL COMMENT '分数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

java部分代码,主要是A、B两个方法进行操作,之所以分成两个方法就是为了测试不同操作同时修改同一条数据会引发的情况,如果是调用的痛一个方法,那直接使用可重入锁或synchronize等处理并发问题就可以解决了

@RestController
@RequestMapping(value = "/score")
public class ScoreController {
    @Autowired
    private ScoreBS scoreBS;

    @PutMapping(value = "/A")
    public void A(){
        scoreBS.A();
    }

    @PutMapping(value = "/B")
    public void B(){
        scoreBS.B();
    }
}
@Service
@Transactional(rollbackFor = Exception.class)
public class ScoreBSImpl implements ScoreBS {
    @Autowired
    private ScoreMapper scoreMapper;

    @Override
    public void A(){

    }
    @Override
    public void B(){

    }
}

需求:甲乙两个用户分别操作A、B方法,两个方法需要先根据id查询出数据,并对score的值进行追加修改

普通写法如下:

@Override
public void A(){
    Score score = scoreMapper.selectById(1);
    score.setScore(score.getScore()+10);
    scoreMapper.updateScore(score);
}
@Override
public void B(){
    Score score = scoreMapper.selectById(1);
    score.setScore(score.getScore()+5);
    scoreMapper.updateScore(score);
}
<select id="selectById" parameterType="java.lang.Integer" resultType="com.tobemn.project.entity.Score">
    select * from score
    where id = #{id,jdbcType=INTEGER}
</select>
<update id="updateScore" parameterType="com.tobemn.project.entity.Score">
    update score
    set score = #{score,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
</update>

普通写法自然只适用于普通操作:用户甲调用A方法之后,用户乙再调用B方法

最终结果:score=15,没毛病,但是如果是如下场景呢

甲乙几乎同时发起调用,AB方法同时查询出score的值,此时A方法还未更新,B方法先更新完了,而后A方法完成更新,结果会是怎么样的

修改A方法

@Override
public void A(){
    Score score = scoreMapper.selectById(1);
    score.setScore(score.getScore()+10);
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    scoreMapper.updateScore(score);
}

最终结果:socre=10

结果跟我们想要的正常流程结果不符,原因就是AB两个方法同时获取了score=0的初始值,而后B将数据库修改为0+5=5,最后A将数据库修改为0+10=10,导致出现了A覆盖了B结果的现象,而且在这种场景下,由于AB是两个独立的方法,没办法通过加可重入锁或synchronize来实现同步,此时FOR UPDATE就派出用场了

修改A方法:

@Override
public void A(){
    Score score = scoreMapper.selectByIdLock(1);
    score.setScore(score.getScore()+10);
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    scoreMapper.updateScore(score);
}

添加带有FOR UPDATE 的sql

<select id="selectById" parameterType="java.lang.Integer" resultType="com.tobemn.project.entity.Score">
    select * from score
    where id = #{id,jdbcType=INTEGER}
    for update
</select>

最终结果:score=5

结果依然不是理想中的score=15,这是因为上面总结的第5点:FOR UPDATE 是写锁操作不会锁住

所以当A方法调用for update时,B方法的查询依然生效,而更新被阻塞,等到A方法执行更新完毕,数据库值是score=10,B的更新方法才开始执行,但是B的更新方法前面获取到的查询值还是初始值0,所以最终结果是score=5

综上所述,想要保障获取到的结果是理想的值,就得当A方法调用for update的时候其它方法在查询是就被阻塞

这就使用到了上面总结的第2点:一个事务获取了某行的排他锁,其他事务就不能再获取该行的锁

修改B方法

@Override
public void B(){
    Score score = scoreMapper.selectByIdLock(1);
    score.setScore(score.getScore()+5);
    scoreMapper.updateScore(score);
}

 

B方法也使用for update进行查询,由于其他事物不能获取已有排他锁的锁,因此会等待当前数据的排他锁被释放才能后进行查询,也就是所谓的,在查询时就进行阻塞

最终结果:score=15

另外一种解决思路就是执行A方法时,同时调用了B方法,不进行阻塞,直接抛出异常,通知用户“服务器繁忙请重试”等,此方法的目的是通过用户重新发起调用来避免并发情况的出现,使用的是关键词FOR UPDATE NOWAIT,此方法暂不推荐在高并发的情况下使用,即影响用户体验,也影响执行速度,仅当做一个思路

 

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
好的,那么以下是一个简单的示例,假设你使用的是Spring Boot和MyBatis: 首先,你需要创建一个Controller类来处理HTTP请求和响应: ```java @RestController @RequestMapping("/chat") public class ChatController { @Autowired private ChitGPT chitGPT; @PostMapping("") public String chat(@RequestBody String inputText) { String response = chitGPT.generateResponse(inputText); return response; } } ``` 在这个例子中,我们使用了Spring Boot的`@RestController`和`@RequestMapping`注释来定义一个处理HTTP请求的Controller类,这个类中包含一个`/chat`路由,当发起POST请求时,会调用`chat()`方法来处理请求。在`chat()`方法中,我们通过使用`@RequestBody`注释来接收POST请求中的输入文本,然后调用`ChitGPT`实例的`generateResponse()`方法来生成回复,并将回复作为HTTP响应返回。 接下来,我们需要在Spring Boot中配置MyBatis。在Spring Boot中,我们可以通过在`application.properties`文件中定义数据源来配置MyBatis。例如: ```properties spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/chitgpt spring.datasource.username=root spring.datasource.password=123456 mybatis.mapper-locations=classpath:mappers/*.xml mybatis.type-aliases-package=com.example.demo.model ``` 在这个例子中,我们使用MySQL数据库作为数据源,在`mybatis.mapper-locations`属性中定义了MyBatis映射文件的位置,`mybatis.type-aliases-package`属性定义了MyBatis类型别名的包名。 最后,我们需要编写MyBatis映射文件来定义数据库表和Java对象之间的映射关系。例如,假设我们有一个名为`chat_history`的表,其中包含`id`、`input_text`和`output_text`三个字段。我们可以编写如下的Mapper接口和XML文件: ```java @Mapper public interface ChatHistoryMapper { @Insert("INSERT INTO chat_history(input_text, output_text) VALUES(#{inputText}, #{outputText})") void insertChatHistory(@Param("inputText") String inputText, @Param("outputText") String outputText); } ``` ```xml <mapper namespace="com.example.demo.mapper.ChatHistoryMapper"> <resultMap id="ChatHistoryMap" type="com.example.demo.model.ChatHistory"> <id property="id" column="id"/> <result property="inputText" column="input_text"/> <result property="outputText" column="output_text"/> </resultMap> <insert id="insertChatHistory" parameterType="map"> INSERT INTO chat_history(input_text, output_text) VALUES(#{inputText}, #{outputText}) </insert> </mapper> ``` 在这个例子中,我们使用MyBatis的注释和XML文件来定义了一个名为`ChatHistoryMapper`的Mapper接口和一个名为`chat_history`的数据表之间的映射关系。我们定义了一个`insertChatHistory()`方法,用于将输入文本和回复文本插入到`chat_history`表中。 当然,这只是一个简单的示例,实际情况中你可能需要更复杂的逻辑来处理请求和生成回复。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

My name is Red ^^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值