項目中用到了MongoDB,准備用來存儲業務數據,前提是要實現事務,保證數據一致性,MongoDB從4.0開始支持事務,提供了面向復制集的多文檔事務特性。能滿足在多個操作,文檔,集合,數據庫之間的事務性,事務的特性。多文檔事務在4.0版本僅支持復制集,對分片集群的事務性支持計划在4.2版本中實現。由於我也算是一個java小白,沒怎么弄清java事務機制,於是先建了個測試項目進行測試。在本例中可以看到多數據源下事務的使用,請重點關注后面記錄的爬坑記。
代碼已上傳到github 傳送門 https://github.com/devmuyuer/trans-demo
Mongo Transaction
項目介紹
-
springboot 2.1.3
-
MongoDB 4.0.3
-
本項目主要為了測試MongoDB事務,由於正式項目還用了其它數據源,所以加入了 Oracle, MySQL的事務,包括多數據源的配置和使用
使用說明
- 1.導入MongoDB的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
- 2.配置MongoDB的連接
spring:
# mongodb 連接
data:
mongodb:
uri: mongodb://192.168.0.68:27017,192.168.0.69:27017,192.168.0.70:27017/glcloud?replicaSet=rs0
database: glcloud
- 3.編寫entity類
當id設置為 ObjectId 類型和添加 @Id 注解時時,MongoDB數據庫會自動生成主鍵,我們在保存對象時就不用設置id的值
MongoUnit
/**
* 用戶
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
@Data
@Document(collection = "test_unit")
public class MongoUnit {
private static final long serialVersionUID = 1L;
/**
* Id
*/
@Id
private ObjectId id;
/**
* unitId
*/
private String unitId;
/**
* unitName
*/
private String unitName;
}
MongoUser
package com.example.demo.entity.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* 用戶
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
@Data
@Document(collection = "test_user")
public class MongoUser {
private static final long serialVersionUID = 1L;
/**
* Id
*/
@Id
private ObjectId id;
/**
* userId
*/
private String userId;
/**
* userName
*/
private String userName;
/**
* unitId 關聯testUser
*/
private String unitId;
}
- 4.編寫dao層的方法
只需繼承MongoRepository即可。
package com.example.demo.repository.mongo;
import com.example.demo.entity.mongo.MongoUser;
import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
public interface MongoUserRepository extends MongoRepository<MongoUser, String> {
}
package com.example.demo.repository.mongo;
import com.example.demo.entity.mongo.MongoUnit;
import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
public interface MongoUnitRepository extends MongoRepository<MongoUnit, String> {
}
- 5.Service層
package com.example.demo.service.mongo.impl;
import com.example.demo.common.SystemException;
import com.example.demo.entity.mongo.MongoUser;
import com.example.demo.repository.mongo.MongoUserRepository;
import com.example.demo.service.mongo.MongoUserService;
import com.example.demo.common.R;
import com.example.demo.common.RUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
@Service
@Slf4j
public class MongoUserServiceImpl implements MongoUserService {
@Autowired
MongoUserRepository mongoUserRepository;
/**
* 新增
* @param mongoUser
* @return
*/
@Override
public R save(MongoUser mongoUser) {
MongoUser mongoUserSave = mongoUserRepository.save(mongoUser);
log.info("用戶信息保存:testUserSave = "+ mongoUserSave);
return RUtil.success("");
}
@Override
@Transactional(value = "MONGO_TRANSACTION_MANAGER", propagation = Propagation.REQUIRED)
public R bathSave(String unitId, Boolean rollBack) {
for (int i = 0; i <= 10; i++) {
//注釋這段則可以正常添加數據,測試回滾則throw異常信息
if (unitId.equals("003") && rollBack) {
throw new SystemException("測試回滾故意拋出的異常");
}
MongoUser user = new MongoUser();
user.setUserId(unitId + "U0" + i);
user.setUserName("用戶" + i);
user.setUnitId(unitId);
save(user);
}
return RUtil.success("");
}
}
package com.example.demo.service.mongo.impl;
import com.example.demo.enums.REnum;
import com.example.demo.common.SystemException;
import com.example.demo.entity.mongo.MongoUnit;
import com.example.demo.repository.mongo.MongoUnitRepository;
import com.example.demo.service.mongo.MongoUnitService;
import com.example.demo.service.mongo.MongoUserService;
import com.example.demo.common.R;
import com.example.demo.common.RUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author muyuer 182443947@qq.com
* @version 1.0
* @date 2019-02-25 09:10
*/
@Service
@Slf4j
public class MongoUnitServiceImpl implements MongoUnitService {
@Autowired
MongoUnitRepository mongoUnitRepository;
@Autowired
MongoUserService mongoUserService;
/**
* 新增
*
* @param unit
* @return
*/
@Override
public R save(MongoUnit unit) {
MongoUnit mongoUnitSave = mongoUnitRepository.save(unit);
log.info("單位信息保存:testUnitSave = " + mongoUnitSave);
return RUtil.success("");
}
@Override
@Transactional(value = "MONGO_TRANSACTION_MANAGER")
public R bathSave(Boolean rollBack) {
try {
for (int i = 0; i < 4; i++) {
MongoUnit unit = new MongoUnit();
unit.setUnitId("00" + i);
unit.setUnitName("單位" + i);
mongoUserService.bathSave(unit.getUnitId(),rollBack);
save(unit);
}
return RUtil.success("");
} catch (SystemException e) {
log.error("保存數據失敗:msg: {}", e.getMessage());
throw new SystemException(REnum.ERROR.getCode(), "保存數據失敗 Error:" + e.getMessage());
}
}
}
- 6.Controller
package com.example.demo.controller;
import com.example.demo.enums.DbTypeEnum;
import com.example.demo.service.mongo.MongoUserService;
import com.example.demo.common.R;
import com.example.demo.service.primary.PrimaryUserService;
import com.example.demo.service.slave.SlaveUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author muyuer 182443947@qq.com
* @date 2019-02-25 10:59
*/
@RestController
@Slf4j
@RequestMapping(path="test/user")
public class TestUserController {
@Autowired
MongoUserService mongoUserService;
@Autowired
PrimaryUserService primaryUserService;
@Autowired
SlaveUserService slaveUserService;
/**
* 新增
* @param dbType
* @param unitId
* @param rollBack
* @return
*/
@PostMapping("/bathSave/{dbType}/{unitId}/{rollBack}")
public R bathSave(@PathVariable DbTypeEnum dbType, @PathVariable String unitId, @PathVariable Boolean rollBack){
switch (dbType) {
case MONGO:
return mongoUserService.bathSave(unitId, rollBack);
case PRIMARY:
return primaryUserService.bathSave(unitId, rollBack);
default:
return slaveUserService.bathSave(unitId, rollBack);
}
}
}
package com.example.demo.controller;
import com.example.demo.enums.DbTypeEnum;
import com.example.demo.service.mongo.MongoUnitService;
import com.example.demo.common.R;
import com.example.demo.service.primary.PrimaryUnitService;
import com.example.demo.service.slave.SlaveUnitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author muyuer 182443947@qq.com
* @date 2019-02-25 10:59
*/
@RestController
@Slf4j
@RequestMapping(path="test/unit")
public class TestUnitController {
@Autowired
MongoUnitService mongoUnitService;
@Autowired
PrimaryUnitService primaryUnitService;
@Autowired
SlaveUnitService slaveUnitService;
/**
* 新增
* @param dbType 數據庫
* @param rollBack 是否回滾
* @return
*/
@PostMapping("/bathSave/{dbType}/{rollBack}")
public R bathSave(@PathVariable DbTypeEnum dbType, @PathVariable Boolean rollBack) {
switch (dbType) {
case MONGO:
return mongoUnitService.bathSave(rollBack);
case PRIMARY:
return primaryUnitService.bathSave(rollBack);
default:
return slaveUnitService.bathSave(rollBack);
}
}
}
測試
PostMan post 地址
MONGO庫 不回滾 http://localhost:8077/test/unit/bathSave/MONGO/0
MONGO庫 回滾 http://localhost:8077/test/unit/bathSave/MONGO/1
Oracle庫 不回滾 http://localhost:8077/test/unit/bathSave/PRIMARY/0
Oracle庫 回滾 http://localhost:8077/test/unit/bathSave/PRIMARY/1
MySQL庫 不回滾 http://localhost:8077/test/unit/bathSave/SLAVE/0
MySQL庫 回滾 http://localhost:8077/test/unit/bathSave/SLAVE/1
在實際應用中爬過的坑
-
1.MongoDB的版本必須是4.0
-
2.MongoDB事務功能必須是在多副本集的情況下才能使用,否則報錯"Sessions are not supported by the MongoDB cluster to which this client is connected",4.2版本會支持分片事務。
-
3.事務控制只能用在已存在的集合中,也就是集合需要手工添加不會由jpa創建會報錯"Cannot create namespace glcloud.test_user in multi-document transaction."
-
4.多數據源時需要指定事務 @Transactional(value = "transactionManager") 如果只有1個數據源不需要指定value
-
5.事務注解到類上時,該類的所有 public 方法將都具有該類型的事務屬性,但一般都是注解到方法上便於實現更精確的事務控制
-
6.事務傳遞性,事務子方法上不必添加事務注解,如果子方法也提供api調用可用注解propagation = Propagation.REQUIRED也就是繼承調用它的事務,如果沒有事務則新起一個事務
-
7.啟動類上的@EnableTransactionManagement注解,並不是像網上所說必需添加的注解,因為spring boot 默認開始了這個注解的。
-
8.有人說:注解必須是@Transactional(rollbackFor = { Exception.class }) 測試並不需要rollbackFor = { Exception.class },因為本例中自定義異常類繼承自RuntimeException spring boot事物默認在遇到RuntimeException不論rollbackFor的異常是啥,都會進行事務的回滾,加上rollbackFor=Exception.class,可以讓事物在遇到非運行時異常時也回滾
具體rollbackFor用法可參考: