事情是这样的,现在遇到一个场景,其实也大多数系统都会遇到的一个场景,就是购买机票的案例。 为了简化问题和保护公司信息安全,把事情简化为在数据库中有个数字,我每次要去读取并更新这个数字,每次加1. 初始值设为0.
设计表为
test {
num int(11) default 0;
}
采用ssm框架,controller层写个TestController
@Controller
public class TestController {
@Autowired
TestService testService;
@RequestMapping(value = "addNum")
@ResponseBody
public HttpRespMsg addNum(
HttpServletResponse response) throws Exception {
HttpRespMsg msg = null;
msg = testService.addNum();
return msg;
}
}
TestService定义如下:
public interface TestService {
public HttpRespMsg addNum();
}
@Transactional
@Service(value="testService")
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public HttpRespMsg addNum() {
HttpRespMsg msg = new HttpRespMsg();
TestExample tExp = new TestExample();
List<Test> list = testMapper.selectByExample(tExp);
if (list.size() == 0) {
Test t = new Test();
t.setNum(1);
testMapper.updateByExampleSelective(t, tExp);
} else {
int oldNum = list.get(0).getNum();
System.out.println(Thread.currentThread().getName() + "读取到oldNum=="+oldNum);
Test t = new Test();
t.setNum(oldNum+1);
testMapper.updateByExampleSelective(t, tExp);
}
msg.data = testMapper.selectByExample(tExp).get(0).getNum();
return msg;
}
}
很明显,在多线程模式下addNum会出现问题。 我用了JMeter模拟多线程并发访问,得到了验证。 20个请求,在1秒内同时调用addNum接口,结果数据库中的num字段值为6(有时候是7,8,5不确定)。问题出在读取和写入不是一个原子性操作。
那怎么解决呢?
首先想到@Transaction,不是有原子性特征吗?为什么不行呢,原来他的原子性指的是对数据库的操作失败的回滚,就是要么全部执行成功,要么都不成功。 并不能处理并发的问题。又想到了isolation隔离性,有READ_COMMITTED, READ_UNCOMMITTED, REPEATABLE_READ, SERIALIZABLE。具体含义此处不做解释,自行百度。 我尝试的结果是,前面三个都无效,最后一个会导致dead lock的异常产生,不能正常更新数据。 至此放弃这种方式。
我又尝试使用代码级别的锁, synchronized关键字来控制。
修改代码如下:
@Transactional
@Service(value="testService")
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public HttpRespMsg addNum() {
HttpRespMsg msg = new HttpRespMsg();
synchronized (TestService.class) {
TestExample tExp = new TestExample();
List<Test> list = testMapper.selectByExample(tExp);
if (list.size() == 0) {
Test t = new Test();
t.setNum(1);
testMapper.updateByExampleSelective(t, tExp);
} else {
int oldNum = list.get(0).getNum();
System.out.println(Thread.currentThread().getName() + "读取到oldNum=="+oldNum);
Test t = new Test();
t.setNum(oldNum+1);
testMapper.updateByExampleSelective(t, tExp);
}
msg.data = testMapper.selectByExample(tExp).get(0).getNum();
}
return msg;
}
}
以为这样是可以的了。赶紧验证下。 重置数据库的num为0,再跑20个同步线程。结果发现num变成了16,有进步,贴近了。但是为什么不是20呢。经过思考和调试,发现线程的锁是起效的,但是在读取的时候,依然会有读取不到最新数据的情况,结合Transaction的特点,一拍脑袋想起来,原来我们的事务是修饰到函数上的,整个函数执行完了才会提交到数据库。 而我们的锁是加载函数里面的代码块上的,范围小于事务。所以同步的代码执行完了,但是数据并没有立马提交。 导致下一个线程读取的时候依旧是老数据。
好吧,那我把synchronized加到addNum上吧。
public synchronized HttpRespMsg addNum(),
接口定义和实现里面都这样改一下,嗯编译没问题。跑一下试试看, opppps. Spring报错了。
修饰符有问题,Spring不允许在service里面的函数前面加这个关键字。只能用public修饰。
最后没办法,加到TestController里的addNum上吧。
@RequestMapping(value = "addNum")
@ResponseBody
public synchronized HttpRespMsg addNum(
HttpServletResponse response) throws Exception {
HttpRespMsg msg = null;
msg = testService.addNum();
return msg;
}
赶紧测试下。
成功!
然而这样会有性能问题,并发时,不能同时执行。是否可以有数据库的存储过程,手动给表加锁等方式解决,有待探索。
分享到:
相关推荐
这套课程既可以作为从零基础开始...课程的主要内容涉及有JAVA基础课程、JAVA多线程与并发编程、数据库开发基础和进阶、Spring Framework、Spring进阶、Spring MVC框架、Spring boot、Java常用类库、Java异常处理等等
spring boot 通过任务执行器 taskexecutor 来实现多线程和并发编程。下面这篇文章主要给大家介绍了关于spring boot中多线程开发的注意事项,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
2021面试题、Java2021面试题、2022面试题、Java2022面试题、Java面试题、JVM面试题、多线程面试题、并发编程、设计模式面试题、Spring面试题、MyBatis面试题、ZooKeepe面试题r、Dubbo面试题、Elasticsearch面试题、...
Java前后开发面试题,大厂进阶之路,基于JavaGuide、Cyc大佬、牛客...包含计算机网络知识、JavaSE、JVM、Spring、Springboot、SpringCloud、Mybatis、多线程并发、netty、MySQL、MongoDB、Elasticsearch、Redis、HBASE
Java前后开发面试题,大厂进阶之路,基于JavaGuide、Cyc大佬、牛客...包含计算机网络知识、JavaSE、JVM、Spring、Springboot、SpringCloud、Mybatis、多线程并发、netty、MySQL、MongoDB、Elasticsearch、Redis、HBASE
2022java面试题、JVM面试题、多线程面试题、并发编程、Redis面试题、MySQL面试题、Java2022面试题、Netty面试题、Elasticsearch面试题、Tomcat面试题、Dubbo面试题、Kafka面试题、Linux面试题、2021面试题、java面试...
第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 ...
了解多线程所带来的安全风险.mp4 从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题...
第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 ...
【一线互联网大厂Java核心面试题库】Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等..
【一线互联网大厂Java核心面试题库】Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等..
BIO,NIO,AIO,Netty面试题 35道.pdf Java并发编程最全面试题 123道.pdf Java并发编程面试题 75题.pdf ...多线程面试59题(含答案).pdf 面试题集(全).pdf 设计模式面试题 14道.pdf Spring面试题 75道.pdf 等等
Java学习笔记包含JVM、spring、源码分析、多线程、offer题解、设计模式、面试宝典.zip Java学习笔记,内容包括JVM,spring,hashMap实现源码分析,多线程,剑指offer题解,设计模式。然后根据面试的重点,又将很多从...
第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 ...
第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 ...
Java前后开发面试题,大厂进阶之路,基于JavaGuide、Cyc大佬、牛客...包含计算机网络知识、JavaSE、JVM、Spring、Springboot、SpringCloud、Mybatis、多线程并发、netty、MySQL、MongoDB、Elasticsearch、Redis、HBASE
Java前后开发面试题,大厂进阶之路,基于JavaGuide、Cyc大佬、牛客...包含计算机网络知识、JavaSE、JVM、Spring、Springboot、SpringCloud、Mybatis、多线程并发、netty、MySQL、MongoDB、Elasticsearch、Redis、HBASE
Linux面试专题及答案+ActiveMQ消息中间件面试专题+Java基础面试题+MySQL性能优化的21个最佳实践+微服务...SpringCloud面试专题及答案+SQL优化面试专题及答案+java多线程并发编程知识导图笔记+Java并发体系知识导图笔记...
- 多线程编程 2. 数据库: - 熟悉SQL语言 - 了解关系型数据库和非关系型数据库 - 数据库连接池 - 数据库事务 3. Spring框架: - Spring Boot - Spring MVC - Spring Data - Spring Security - Spring...