背景:
当我们需要对数据进行先读取,满足某条件再做新增,往往会面临着线程不安全的问题,导致数据被重复插入。
下面分别举例子来说明单实例与多实例(集群)下的保证数据安全。
需要用到的工具:
1、并发测试工具JMeter,模拟多用户并发请求,也就是多个用户在同一时刻同时情求该接口。
2、ngnix (1.12.2版本) 本地组建集群服务,通过配置完成,来自用户的请求,从多个服务节点选择一个来完成该请求响应。nginx会自动做分发,从而达到负载均衡。
需求:
插入80个项目实例到mysql 的project 表中。
一、单节点的情况
主要代码如下:
service层 class ProjectServiceImpl中:
public void addProject2(Project project) {
int maxCount =100;
int count = 80;
public void addProject2(Project project) {
for (int i = 0; i < maxCount; i++) {
Date date = new Date();
List<Project> projectList = selectProjects(project);
if (projectList.size() >= count) {
return;
}
project.setId(UUID.randomUUID().toString().replaceAll("-", ""));
project.setProjectName(project.getId());
project.setCreateTime(date);
project.setModifyTime(date);
project.setModifier("1");
project.setCreator("1");
insertProject(project);
}
}
控制器层的代码:
/**
* 新增项目
*/
@PostMapping("addProject")
public RestMessage addProject(@RequestBody Project project) {
service.addProject2(project);
return RestBuilders.successBuilder().build();
}
打开postman开多个tab快速的依次切换tab并点击send,均成功返回:(此处也可以用JMeter测试)
去navicat 查看数据发现产生了81条数据,而不是80条。其原因就是线程不安全所致,查询与插入的动作没有在一个同步代码块中
单节点解决上述问题方案:方法加 synchronized 修饰,也可以加在代码块,还可以用同步锁实现。
修改上述addProject2 方法如下:
synchronized public void addProject2(Project project) {
//xxx 省略相同部分
}
此处我们改用更专业的JMeter软件来测试:设置200个线程(相当于上述步骤的postman开200个窗口同时点击的效果)
htpp 请求设置如下:
去数据库工具navicat 查看有多少条该数据:相同project_code共有80条整:
结论:
对于单机服务(单节点),加synchronized 修饰方法,或采用同步代码块,或同步锁可以解决此类问题:代码中线程不安全导致数据重复插入的问题。
一、集群多节点的情况
还是上述代码不改,继续保留synchronized 修饰方法,现在在idea中起两个服务 LinkappApiApplication 服务端口是5042 LInkappApiApplication-B的服务端口是5043
nginx 集群配置如下,现在是两个节点 127.0.0.1:5042 和127.0.0.1:5043
启动nginx
打开JMeter请求,依然是设置200个线程(模拟200个用户并发)
请求插入项目接口 http://localhost:5042/project/addProject 的请求参数如下:
{
"projectCode": "test_data_3"
}
待请求结束后去mysql 查看插入了多少条:结果却显示插入了81条:仍然出现了重复插入的问题:可以看到即使加了synchronized修饰方法仍然有重复插入
问题:如何解决多节点的情况下,数据重复插入的问题?
解答:这里需要用到分布式锁,数字在做读和写的时候需要用分布式锁将读写代码锁住。分布式锁的实现方式有很多种,这里采用基于redis的redisson锁
需要在pom中引入redisson依赖:
依赖注入对象:
对上面单节点的新增项目的代码的改造:将addProject2 放在addProject方法中调用:
修改完成后测试:
依然采用JMeter 设置200个线程测试:
去数据库查看插入的数据:不多不少刚好80条:有效果。
多次测试将JMeter进程数设置为500 ,仍然数据正确,刚好80条插入成功。
原创博文, 如需转载请言明出处,谢谢!