`
seya
  • 浏览: 356165 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

多线程并发下的Spring编程

 
阅读更多
事情是这样的,现在遇到一个场景,其实也大多数系统都会遇到的一个场景,就是购买机票的案例。 为了简化问题和保护公司信息安全,把事情简化为在数据库中有个数字,我每次要去读取并更新这个数字,每次加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;
}
赶紧测试下。
成功!
然而这样会有性能问题,并发时,不能同时执行。是否可以有数据库的存储过程,手动给表加锁等方式解决,有待探索。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics