MySQL数据库update A set num=num+ ? where id=?并发执行情况下存在线程安全问题吗?
首先,先说结论:这个SQL语句执行可以看做是原子操作,没有线程安全问题。
使用代码演示一下:
1.创建一个xu_hao表,为了简单,只有id和num两个字段
CREATE TABLE `xu_hao` (
`id` int(11) NOT NULL,
`num` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2.插入一条初始化数据:
INSERT INTO `xu_hao`(`id`, `num`) VALUES (1, 0);
3.创建一个spring boot工程,项目结构如下:
4.导入maven依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
5.编写application.yml配置文件,测试项目,简单配置即可:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/learn_work?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
logging:
level:
root: warn
com.learn.spmysqltest.mapper: trace
6.创建一个XuHaoMapper:
public interface XuHaoMapper {
/**
* 自增+1的方式更新num
*
* @param id
* @return
*/
@Update("update xu_hao set num = num + 1 where id = #{id}")
int updateXuHaoByIncrease(int id) throws Exception;
/**
* 通过id直接更新num
*
* @param num
* @param id
* @return
* @throws Exception
*/
@Update("update xu_hao set num = #{num} where id = #{id}")
int updateXuHao(int num, int id) throws Exception;
/**
* 通过id查询num
*
* @param id
* @return
* @throws Exception
*/
@Select("select num from xu_hao where id = #{id} ")
int selectXuHao(int id) throws Exception;
}
7.创建XuHaoService:
public interface XuHaoService {
/**
* 自增+1的方式更新num
*
* @param id
* @return
*/
int updateXuHaoByIncrease(int id) throws Exception;
/**
* 直接更新num
*
* @param id
* @throws Exception
*/
int updateXuHao(int id) throws Exception;
/**
* 通过id查询num
*
* @param id
* @return
* @throws Exception
*/
int getXuHao(int id) throws Exception;
}
8.创建XuHaoServiceImpl:
@Service
public class XuHaoServiceImpl implements XuHaoService {
@Autowired
private XuHaoMapper xuHaoMapper;
@Override
@Transactional
public int updateXuHaoByIncrease(int id) throws Exception {
return xuHaoMapper.updateXuHaoByIncrease(id);
}
@Override
@Transactional
public int updateXuHao(int id) throws Exception {
int dbNum = xuHaoMapper.selectXuHao(id);
dbNum++; // num + 1
return xuHaoMapper.updateXuHao(dbNum, id);
}
@Override
@Transactional(readOnly = true)
public int getXuHao(int id) throws Exception {
return xuHaoMapper.selectXuHao(id);
}
}
9.开启事务支持:
@SpringBootApplication
@EnableTransactionManagement // 开启事务
@MapperScan("com.learn.spmysqltest.mapper")
public class SpMysqlTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpMysqlTestApplication.class, args);
}
}
由于单元测试多线程可能会有问题,为了简单,编写一个XuHaoController进行测试:
@RestController
@RequestMapping("/xuhao")
public class XuHaoController {
@Autowired
private XuHaoService xuHaoService;
/**
* 测试 update xu_hao set num = num + 1 where id = 1
*
* @return
*/
@GetMapping("/updateadd1")
public String updateByIncrease() {
int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
CountDownLatch latch = new CountDownLatch(cupNum);
for (int i = 0; i < cupNum; i++) {
new Thread(() -> {
try {
latch.await();
xuHaoService.updateXuHaoByIncrease(1);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
return "cupNum=" + cupNum + ",预期num=" + cupNum;
}
/**
* num = num + 1
* 测试 update xu_hao set num = num where id = 1
*
* @return
*/
@GetMapping("/update")
public String update() {
int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
CountDownLatch latch = new CountDownLatch(cupNum);
for (int i = 0; i < cupNum; i++) {
new Thread(() -> {
try {
latch.await();
xuHaoService.updateXuHao(1);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
return "cupNum=" + cupNum + ",预期num=" + cupNum;
}
}
进入正式测试,访问:http://127.0.0.1:8080/xuhao/updateadd1,运行结果:
数据库:
可以看到,我电脑处理器个数是6个,我用CountDownLatch模拟6个线程同时执行数据库更新操作,预期值和处理器个数一致,并没有出现线程安全问题。
简单解释一下:每次执行update的时候,会持有id=1这行记录的排它锁,后面的update需要持有这个记录的排它锁释放之后,才能对他进行修改,也就是说,后面的update操作会阻塞,直到前一个update提交成功或者发生异常回滚事务了,他才会获得这个锁,从而对数据进行修改。(可以网上查询更加详细的解答)
如果我们是查询num后,在代码里面进行加1操作,再更新回数据库呢?把数据库中的num重新置为0,访问:http://127.0.0.1:8080/xuhao/update,数据库结果:
预期是6,现在却是1,很明显有线程安全问题,有其他的解决方法吗?
其实有了上面的启示,我们很容易想到,使用MySQL数据库的排它锁来解决(select … for update),在XuHaoMapper中增加一个函数:
/**
* 通过id查询num
*
* @param id
* @return
* @throws Exception
*/
@Select("select num from xu_hao where id = #{id} for update ")
int selectXuHao2(int id) throws Exception;
修改XuHaoServiceImpl中的updateXuHao方法:
@Override
@Transactional
public int updateXuHao(int id) throws Exception {
// int dbNum = xuHaoMapper.selectXuHao(id);
int dbNum = xuHaoMapper.selectXuHao2(id);
dbNum++; // num + 1
return xuHaoMapper.updateXuHao(dbNum, id);
}
重新将数据库中num置为0,并访问http://127.0.0.1:8080/xuhao/update,数据库结果:
可以看到,数据库结果与预期结果一致,此时也没有并发访问问题,原因与上面相似,for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则必须获取该行级锁.行锁永远是独占方式锁。for update 需要事务提交或回滚了才会释放锁,所以在spring管理的事务方法中,使用这种方式,也不会出现并发访问问题。
注意:select … for update ,如果查询条件带有主键,会锁行数据,如果没有,会锁表,另外并发访问下可能会造成死锁!