Synchronized+Spring事務(wù) == 線程不安全??
某日進(jìn)行多線程實(shí)踐時(shí),突發(fā)奇想將@Transactional注解的spring事務(wù)方法用synchronized進(jìn)行了修飾。
那么理論上,事務(wù)方法指向時(shí)應(yīng)當(dāng)是線程安全的。然而在執(zhí)行如下方法時(shí),最終生成的訂單數(shù)>商品的剩余數(shù)量。
@Service
public class SecKillServiceImpl implements SecKillService {
@Autowired
SecKillMapper mapper;
@Transactional
@Override
public synchronized void SecKillProduct(String productId) {
//查詢剩余庫存
Product product1 = mapper.queryProduceById(productId);
if(product1.getStock()>0){
product1.setStock(product1.getStock()-1);
//更新庫存
mapper.SecKillProduct(product1);
//創(chuàng)建對(duì)應(yīng)訂單
mapper.CreateOrder(product1.getProductId(), UUID.randomUUID().toString().substring(0, 25));
}
}
}
如上方法被synchronized修飾,那么多個(gè)不同的線程進(jìn)來不是應(yīng)該排隊(duì)串行執(zhí)行mapper方法?
結(jié)論顯然不是。
mapper和controller應(yīng)該也沒有問題
<mapper namespace="com.ed.concurrency.cdemo.mapper.SecKillMapper">
<update id="SecKillProduct">
update product_list
set stock = #{stock}
where id = #{productId}
</update>
<resultMap id="productMap" type="com.ed.concurrency.cdemo.POJO.Product">
<result column="id" property="productId"/>
<result column="stock" property="stock"/>
</resultMap>
<select id="queryProduceById" resultMap="productMap">
select id, stock from unamed.product_list where id = #{id}
</select>
<insert id="CreateOrder">
insert into unamed.order_list (produce_id, order_id) values (#{productId}, #{orderId})
</insert>
</mapper>
Controller
@RestController
public class SecKillController {
@Autowired
SecKillService service;
@RequestMapping("/seckill/{id}")
public String SecKill(@PathVariable("id") String productId){
try{
service.SecKillProduct(productId);
}catch (Exception e){
e.printStackTrace();
return "抱歉商品已經(jīng)售完";
}
return "商品秒殺成功";
}
}
Synchronized應(yīng)該是安全的
@Transactional里面的syn并不能給整個(gè)事務(wù)加鎖
synchronized加在了這個(gè)方法上,但不是事務(wù)上。
為了說明這點(diǎn)需要先明白spring的事務(wù)是如何實(shí)現(xiàn)的。
Spring事務(wù)的底層是Spring AOP, 而Spring AOP的底層是動(dòng)態(tài)代理實(shí)現(xiàn)。
回顧如下代碼:
public static void main(String[] args) {
// 目標(biāo)對(duì)象
Object target ;
Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 但凡帶有@Transcational注解的方法都會(huì)被攔截
try{
// 1... 開啟事務(wù)
method.invoke(target);
// 2... 提交事務(wù)
}catch(Exception e){
//異常處理
}finally{
//最終處理
}
return null;
}
});
}
不難發(fā)現(xiàn),我們所添加的synchronized關(guān)鍵字實(shí)際作用在method.invoke(target)這行代碼上。而整個(gè)動(dòng)態(tài)代理過程卻不是線程安全的。
- 同時(shí),另外一個(gè)重要的點(diǎn)是:Spring的事務(wù)是執(zhí)行成功后,才一次性寫入到Mysql中(或其他數(shù)據(jù)庫)
- 所以注意區(qū)分Spring中的事務(wù)和Mysql中的事務(wù),Spring的事務(wù)是完全在Spring中控制的,與Mysql的事務(wù)沒有任何關(guān)系
那么多線程下,不安全的情況就出現(xiàn)了:如果被事務(wù)修飾的方法執(zhí)行完了synchronized方法,但還未提交事務(wù)(也就是沒有寫入到數(shù)據(jù)庫中),而此時(shí)其他的線程進(jìn)入synchronized方法就獲取到了實(shí)際上已經(jīng)過時(shí)的數(shù)據(jù)。
問題如何解決
知道了聲明式事務(wù)修飾的方法是線程不安全的,那么在調(diào)用service出添加synchronized同步(也就是controller層)就可以解決這里線程不安全的問題。
@RequestMapping("/seckill/{id}")
public String SecKill(@PathVariable("id") String productId){
try{
synchronized (SecKillService.class){
service.SecKillProduct(productId);
}
}catch (Exception e){
e.printStackTrace();
return "抱歉商品已經(jīng)售完";
}
return "商品秒殺成功";
}
當(dāng)然,正常的并發(fā)控制應(yīng)該不會(huì)這么解決。
奇怪的知識(shí)點(diǎn)增加了。