SpringCloud認識五之分布式鎖和分布式事務


SpringCloud認識五之分布式鎖和分布式事務

https://blog.csdn.net/weixin_41446894/article/details/86260854

本人講述的是基於 Spring Cloud 的分布式架構,那么也帶來了線程安全問題,比如一個商城系統,下單過程可能由不同的微服務協作完成,在高並發的情況下如果不加鎖就會有問題,而傳統的加鎖方式只針對單一架構,對於分布式架構是不適合的,這時就需要用到分布式鎖。

實現分布式鎖的方式有很多,結合我的實際項目和目前的技術趨勢,通過實例實現幾種較為流行的分布式鎖方案,最后會對不同的方案進行比較。

基於 Redis 的分布式鎖
利用 SETNX 和 SETEX

基本命令主要有:

SETNX(SET If Not Exists):當且僅當 Key 不存在時,則可以設置,否則不做任何動作。
SETEX:可以設置超時時間
其原理為:通過 SETNX 設置 Key-Value 來獲得鎖,隨即進入死循環,每次循環判斷,如果存在 Key 則繼續循環,如果不存在 Key,則跳出循環,當前任務執行完成后,刪除 Key 以釋放鎖。

這種方式可能會導致死鎖,為了避免這種情況,需要設置超時時間。

下面,請看具體的實現步驟。

1.創建一個 Maven 工程並在 pom.xml 加入以下依賴:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- 開啟web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.創建啟動類 Application.java:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}

}
3.添加配置文件 application.yml:

server:
port: 8080
spring:
redis:
host: localhost
port: 6379
4.創建全局鎖類 Lock.java:

/**
* 全局鎖,包括鎖的名稱
*/
public class Lock {
private String name;
private String value;

public Lock(String name, String value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}

public String getValue() {
return value;
}

}
5.創建分布式鎖類 DistributedLockHandler.java:

import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component;

@Component
public class DistributedLockHandler {

private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler.class);
private final static long LOCK_EXPIRE = 30 * 1000L;//單個業務持有鎖的時間30s,防止死鎖
private final static long LOCK_TRY_INTERVAL = 30L;//默認30ms嘗試一次
private final static long LOCK_TRY_TIMEOUT = 20 * 1000L;//默認嘗試20s

@Autowired
private StringRedisTemplate template;

/**
* 嘗試獲取全局鎖
*
* @param lock 鎖的名稱
* @return true 獲取成功,false獲取失敗
*/
public boolean tryLock(Lock lock) {
return getLock(lock, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}

/**
* 嘗試獲取全局鎖
*
* @param lock 鎖的名稱
* @param timeout 獲取超時時間 單位ms
* @return true 獲取成功,false獲取失敗
*/
public boolean tryLock(Lock lock, long timeout) {
return getLock(lock, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}

/**
* 嘗試獲取全局鎖
*
* @param lock 鎖的名稱
* @param timeout 獲取鎖的超時時間
* @param tryInterval 多少毫秒嘗試獲取一次
* @return true 獲取成功,false獲取失敗
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval) {
return getLock(lock, timeout, tryInterval, LOCK_EXPIRE);
}

/**
* 嘗試獲取全局鎖
*
* @param lock 鎖的名稱
* @param timeout 獲取鎖的超時時間
* @param tryInterval 多少毫秒嘗試獲取一次
* @param lockExpireTime 鎖的過期
* @return true 獲取成功,false獲取失敗
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
return getLock(lock, timeout, tryInterval, lockExpireTime);
}


/**
* 操作redis獲取全局鎖
*
* @param lock 鎖的名稱
* @param timeout 獲取的超時時間
* @param tryInterval 多少ms嘗試一次
* @param lockExpireTime 獲取成功后鎖的過期時間
* @return true 獲取成功,false獲取失敗
*/
public boolean getLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
try {
if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {
return false;
}
long startTime = System.currentTimeMillis();
do{
if (!template.hasKey(lock.getName())) {
ValueOperations<String, String> ops = template.opsForValue();
ops.set(lock.getName(), lock.getValue(), lockExpireTime, TimeUnit.MILLISECONDS);
return true;
} else {//存在鎖
logger.debug("lock is exist!!!");
}
if (System.currentTimeMillis() - startTime > timeout) {//嘗試超過了設定值之后直接跳出循環
return false;
}
Thread.sleep(tryInterval);
}
while (template.hasKey(lock.getName())) ;
} catch (InterruptedException e) {
logger.error(e.getMessage());
return false;
}
return false;
}

/**
* 釋放鎖
*/
public void releaseLock(Lock lock) {
if (!StringUtils.isEmpty(lock.getName())) {
template.delete(lock.getName());
}
}

}
6.最后創建 HelloController 來測試分布式鎖。

@RestController
public class HelloController {

@Autowired
private DistributedLockHandler distributedLockHandler;

@RequestMapping("index")
public String index(){
Lock lock=new Lock("lynn","min");
if(distributedLockHandler.tryLock(lock)){
try {
//為了演示鎖的效果,這里睡眠5000毫秒
System.out.println("執行方法");
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
distributedLockHandler.releaseLock(lock);
}
return "hello world!";
}
}
7.測試。

啟動 Application.java,連續訪問兩次瀏覽器:http://localhost:8080/index,控制台可以發現先打印了一次“執行方法”,說明后面一個線程被鎖住了,5秒后又再次打印了“執行方法”,說明鎖被成功釋放。

通過這種方式創建的分布式鎖存在以下問題:

高並發的情況下,如果兩個線程同時進入循環,可能導致加鎖失敗。
SETNX 是一個耗時操作,因為它需要判斷 Key 是否存在,因為會存在性能問題。
因此,Redis 官方推薦 Redlock 來實現分布式鎖。

利用 Redlock

通過 Redlock 實現分布式鎖比其他算法更加可靠,繼續改造上一例的代碼。

1.pom.xml 增加以下依賴:

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.0</version>
</dependency>
2.增加以下幾個類:

/**
* 獲取鎖后需要處理的邏輯
*/
public interface AquiredLockWorker<T> {
T invokeAfterLockAquire() throws Exception;
}
/**
* 獲取鎖管理類
*/
public interface DistributedLocker {

/**
* 獲取鎖
* @param resourceName 鎖的名稱
* @param worker 獲取鎖后的處理類
* @param <T>
* @return 處理完具體的業務邏輯要返回的數據
* @throws UnableToAquireLockException
* @throws Exception
*/
<T> T lock(String resourceName, AquiredLockWorker<T> worker) throws UnableToAquireLockException, Exception;

<T> T lock(String resourceName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception;

}
/**
* 異常類
*/
public class UnableToAquireLockException extends RuntimeException {

public UnableToAquireLockException() {
}

public UnableToAquireLockException(String message) {
super(message);
}

public UnableToAquireLockException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 獲取RedissonClient連接類
*/
@Component
public class RedissonConnector {
RedissonClient redisson;
@PostConstruct
public void init(){
redisson = Redisson.create();
}

public RedissonClient getClient(){
return redisson;
}

}
@Component
public class RedisLocker implements DistributedLocker{

private final static String LOCKER_PREFIX = "lock:";

@Autowired
RedissonConnector redissonConnector;
@Override
public <T> T lock(String resourceName, AquiredLockWorker<T> worker) throws InterruptedException, UnableToAquireLockException, Exception {

return lock(resourceName, worker, 100);
}

@Override
public <T> T lock(String resourceName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception {
RedissonClient redisson= redissonConnector.getClient();
RLock lock = redisson.getLock(LOCKER_PREFIX + resourceName);
// Wait for 100 seconds seconds and automatically unlock it after lockTime seconds
boolean success = lock.tryLock(100, lockTime, TimeUnit.SECONDS);
if (success) {
try {
return worker.invokeAfterLockAquire();
} finally {
lock.unlock();
}
}
throw new UnableToAquireLockException();
}
}
3.修改 HelloController:

@RestController
public class HelloController {

@Autowired
private DistributedLocker distributedLocker;

@RequestMapping("index")
public String index()throws Exception{
distributedLocker.lock("test",new AquiredLockWorker<Object>() {

@Override
public Object invokeAfterLockAquire() {
try {
System.out.println("執行方法!");
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
return null;
}

});
return "hello world!";
}
}
4.按照上節的測試方法進行測試,我們發現分布式鎖也生效了。

Redlock 是 Redis 官方推薦的一種方案,因此可靠性比較高。

基於數據庫的分布式鎖
基於數據庫表

它的基本原理和 Redis 的 SETNX 類似,其實就是創建一個分布式鎖表,加鎖后,我們就在表增加一條記錄,釋放鎖即把該數據刪掉,具體實現,我這里就不再一一舉出。

它同樣存在一些問題:

沒有失效時間,容易導致死鎖;
依賴數據庫的可用性,一旦數據庫掛掉,鎖就馬上不可用;
這把鎖只能是非阻塞的,因為數據的 insert 操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作;
這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數據庫中數據已經存在了。
樂觀鎖

基本原理為:樂觀鎖一般通過 version 來實現,也就是在數據庫表創建一個 version 字段,每次更新成功,則 version+1,讀取數據時,我們將 version 字段一並讀出,每次更新時將會對版本號進行比較,如果一致則執行此操作,否則更新失敗!

悲觀鎖(排他鎖)

實現步驟見下面說明。

1.創建一張數據庫表:

CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
2.通過數據庫的排他鎖來實現分布式鎖。

基於 MySQL 的 InnoDB 引擎,可以使用以下方法來實現加鎖操作:

public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from methodLock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){

}
sleep(1000);
}
return false;
}
3.我們可以認為獲得排它鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執行方法的業務邏輯,執行完方法之后,再通過以下方法解鎖:

public void unlock(){
connection.commit();
}
基於 Zookeeper 的分布式鎖
ZooKeeper 簡介

ZooKeeper 是一個分布式的,開放源碼的分布式應用程序協調服務,是 Google Chubby 的一個開源實現,是 Hadoop 和 Hbase 的重要組件。它是一個為分布式應用提供一致性服務的軟件,提供的功能包括:配置維護、域名服務、分布式同步、組服務等。

分布式鎖實現原理

實現原理為:

建立一個節點,假如名為 lock 。節點類型為持久節點(Persistent)
每當進程需要訪問共享資源時,會調用分布式鎖的 lock() 或 tryLock() 方法獲得鎖,這個時候會在第一步創建的 lock 節點下建立相應的順序子節點,節點類型為臨時順序節點(EPHEMERAL_SEQUENTIAL),通過組成特定的名字 name+lock+順序號。
在建立子節點后,對 lock 下面的所有以 name 開頭的子節點進行排序,判斷剛剛建立的子節點順序號是否是最小的節點,假如是最小節點,則獲得該鎖對資源進行訪問。
假如不是該節點,就獲得該節點的上一順序節點,並監測該節點是否存在注冊監聽事件。同時在這里阻塞。等待監聽事件的發生,獲得鎖控制權。
當調用完共享資源后,調用 unlock() 方法,關閉 ZooKeeper,進而可以引發監聽事件,釋放該鎖。
實現的分布式鎖是嚴格的按照順序訪問的並發鎖。

代碼實現

我們繼續改造本文的工程。

1.創建 DistributedLock 類:

public class DistributedLock implements Lock, Watcher{
private ZooKeeper zk;
private String root = "/locks";//根
private String lockName;//競爭資源的標志
private String waitNode;//等待前一個鎖
private String myZnode;//當前鎖
private CountDownLatch latch;//計數器
private CountDownLatch connectedSignal=new CountDownLatch(1);
private int sessionTimeout = 30000;
/**
* 創建分布式鎖,使用前請確認config配置的zookeeper服務可用
* @param config localhost:2181
* @param lockName 競爭資源標志,lockName中不能包含單詞_lock_
*/
public DistributedLock(String config, String lockName){
this.lockName = lockName;
// 創建一個與服務器的連接
try {
zk = new ZooKeeper(config, sessionTimeout, this);
connectedSignal.await();
Stat stat = zk.exists(root, false);//此去不執行 Watcher
if(stat == null){
// 創建根節點
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
/**
* zookeeper節點的監視器
*/
public void process(WatchedEvent event) {
//建立連接用
if(event.getState()== Event.KeeperState.SyncConnected){
connectedSignal.countDown();
return;
}
//其他線程放棄鎖的標志
if(this.latch != null) {
this.latch.countDown();
}
}

public void lock() {
try {
if(this.tryLock()){
System.out.println("Thread " + Thread.currentThread().getId() + " " +myZnode + " get lock true");
return;
}
else{
waitForLock(waitNode, sessionTimeout);//等待鎖
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
String splitStr = "_lock_";
if(lockName.contains(splitStr))
throw new LockException("lockName can not contains \\u000B");
//創建臨時子節點
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(myZnode + " is created ");
//取出所有子節點
List<String> subNodes = zk.getChildren(root, false);
//取出所有lockName的鎖
List<String> lockObjNodes = new ArrayList<String>();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if(_node.equals(lockName)){
lockObjNodes.add(node);
}
}
Collections.sort(lockObjNodes);

if(myZnode.equals(root+"/"+lockObjNodes.get(0))){
//如果是最小的節點,則表示取得鎖
System.out.println(myZnode + "==" + lockObjNodes.get(0));
return true;
}
//如果不是最小的節點,找到比自己小1的節點
String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);//找到前一個子節點
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
public boolean tryLock(long time, TimeUnit unit) {
try {
if(this.tryLock()){
return true;
}
return waitForLock(waitNode,time);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(root + "/" + lower,true);//同時注冊監聽。
//判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時注冊監聽
if(stat != null){
System.out.println("Thread " + Thread.currentThread().getId() + " waiting for " + root + "/" + lower);
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);//等待,這里應該一直等待其他線程釋放鎖
this.latch = null;
}
return true;
}
public void unlock() {
try {
System.out.println("unlock " + myZnode);
zk.delete(myZnode,-1);
myZnode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public void lockInterruptibly() throws InterruptedException {
this.lock();
}
public Condition newCondition() {
return null;
}

public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e){
super(e);
}
public LockException(Exception e){
super(e);
}
}
}
2.改造 HelloController.java:

@RestController
public class HelloController {

@RequestMapping("index")
public String index()throws Exception{
DistributedLock lock = new DistributedLock("localhost:2181","lock");
lock.lock();
//共享資源
if(lock != null){
System.out.println("執行方法");
Thread.sleep(5000);
lock.unlock();
}
return "hello world!";
}
}
3.按照本文 Redis 分布式鎖的方法測試,我們發現同樣成功加鎖了。

總結
通過以上的實例可以得出以下結論:

通過數據庫實現分布式鎖是最不可靠的一種方式,對數據庫依賴較大,性能較低,不利於處理高並發的場景。
通過 Redis 的 Redlock 和 ZooKeeper 來加鎖,性能有了比較大的提升。
針對 Redlock,曾經有位大神對其實現的分布式鎖提出了質疑,但是 Redis 官方卻不認可其說法,所謂公說公有理婆說婆有理,對於分布式鎖的解決方案,沒有最好,只有最適合的,根據不同的項目采取不同方案才是最合理的。
首先我們應知道,事務是為了保證數據的一致性而產生的。那么分布式事務,顧名思義,就是我們要保證分布在不同數據庫、不同服務器、不同應用之間的數據一致性。

為什么需要分布式事務?
最傳統的架構是單一架構,數據是存放在一個數據庫上的,采用數據庫的事務就能滿足我們的要求。隨着業務的不斷擴張,數據的不斷增加,單一數據庫已經到達了一個瓶頸,因此我們需要對數據庫進行分庫分表。為了保證數據的一致性,可能需要不同的數據庫之間的數據要么同時成功,要么同時失敗,否則可能導致產生一些臟數據,也可能滋生 Bug。

在這種情況下,分布式事務思想應運而生。

應用場景
分布式事務的應用場景很廣,我也無法一一舉例,我列舉出比較常見的場景,以便於讀者在實際項目中,在用到了一些場景時即可考慮分布式事務。

支付

最經典的場景就是支付了,一筆支付,是對買家賬戶進行扣款,同時對賣家賬戶進行加錢,這些操作必須在一個事務里執行,要么全部成功,要么全部失敗。而對於買家賬戶屬於買家中心,對應的是買家數據庫,而賣家賬戶屬於賣家中心,對應的是賣家數據庫,對不同數據庫的操作必然需要引入分布式事務。

在線下單

買家在電商平台下單,往往會涉及到兩個動作,一個是扣庫存,第二個是更新訂單狀態,庫存和訂單一般屬於不同的數據庫,需要使用分布式事務保證數據一致性。

銀行轉賬

賬戶 A 轉賬到賬戶 B,實際操作是賬戶 A 減去相應金額,賬戶 B 增加相應金額,在分庫分表的前提下,賬戶 A 和賬戶 B 可能分別存儲在不同的數據庫中,這時需要使用分布式事務保證數據庫一致性。否則可能導致的后果是 A 扣了錢 B 卻沒有增加錢,或者 B 增加了錢 A 卻沒有扣錢。

SpringBoot 集成 Atomikos 實現分布式事務
Atomikos 簡介

Atomikos 是一個為 Java 平台提供增值服務的開源類事務管理器。

以下是包括在這個開源版本中的一些功能:

全面崩潰 / 重啟恢復;
兼容標准的 SUN 公司 JTA API;
嵌套事務;
為 XA 和非 XA 提供內置的 JDBC 適配器。
注釋:XA 協議由 Tuxedo 首先提出的,並交給 X/Open 組織,作為資源管理器(數據庫)與事務管理器的接口標准。目前,Oracle、Informix、DB2 和 Sybase 等各大數據庫廠家都提供對 XA 的支持。XA 協議采用兩階段提交方式來管理分布式事務。XA 接口提供資源管理器與事務管理器之間進行通信的標准接口。XA 協議包括兩套函數,以 xa_ 開頭的及以 ax_ 開頭的。

具體實現

1.在本地創建兩個數據庫:test01,test02,並且創建相同的數據庫表:

 

2.改造上篇的工程,在 pom.xml 增加以下依賴:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
3.修改配置文件 application.yml 如下:

server:
port: 8080
spring:
redis:
host: localhost
port: 6379
mysql:
datasource:
test1:
url: jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8
username: root
password: 1qaz2wsx
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
testQuery: select 1
test2:
url: jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8
username: root
password: 1qaz2wsx
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
testQuery: select 1
4.創建以下類:

@ConfigurationProperties(prefix = "mysql.datasource.test1")
@SpringBootConfiguration
public class DBConfig1 {

private String url;
private String username;
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMinPoolSize() {
return minPoolSize;
}
public void setMinPoolSize(int minPoolSize) {
this.minPoolSize = minPoolSize;
}
public int getMaxPoolSize() {
return maxPoolSize;
}
public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
}
public int getMaxLifetime() {
return maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
this.maxLifetime = maxLifetime;
}
public int getBorrowConnectionTimeout() {
return borrowConnectionTimeout;
}
public void setBorrowConnectionTimeout(int borrowConnectionTimeout) {
this.borrowConnectionTimeout = borrowConnectionTimeout;
}
public int getLoginTimeout() {
return loginTimeout;
}
public void setLoginTimeout(int loginTimeout) {
this.loginTimeout = loginTimeout;
}
public int getMaintenanceInterval() {
return maintenanceInterval;
}
public void setMaintenanceInterval(int maintenanceInterval) {
this.maintenanceInterval = maintenanceInterval;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public String getTestQuery() {
return testQuery;
}
public void setTestQuery(String testQuery) {
this.testQuery = testQuery;
}

}
@ConfigurationProperties(prefix = "mysql.datasource.test2")
@SpringBootConfiguration
public class DBConfig2 {

private String url;
private String username;
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMinPoolSize() {
return minPoolSize;
}
public void setMinPoolSize(int minPoolSize) {
this.minPoolSize = minPoolSize;
}
public int getMaxPoolSize() {
return maxPoolSize;
}
public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
}
public int getMaxLifetime() {
return maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
this.maxLifetime = maxLifetime;
}
public int getBorrowConnectionTimeout() {
return borrowConnectionTimeout;
}
public void setBorrowConnectionTimeout(int borrowConnectionTimeout) {
this.borrowConnectionTimeout = borrowConnectionTimeout;
}
public int getLoginTimeout() {
return loginTimeout;
}
public void setLoginTimeout(int loginTimeout) {
this.loginTimeout = loginTimeout;
}
public int getMaintenanceInterval() {
return maintenanceInterval;
}
public void setMaintenanceInterval(int maintenanceInterval) {
this.maintenanceInterval = maintenanceInterval;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public String getTestQuery() {
return testQuery;
}
public void setTestQuery(String testQuery) {
this.testQuery = testQuery;
}

}
@SpringBootConfiguration
@MapperScan(basePackages = "com.lynn.demo.test01", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisConfig1 {

// 配置數據源
@Primary
@Bean(name = "dataSource")
public DataSource dataSource(DBConfig1 config) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(config.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(config.getPassword());
mysqlXaDataSource.setUser(config.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("dataSource");

xaDataSource.setMinPoolSize(config.getMinPoolSize());
xaDataSource.setMaxPoolSize(config.getMaxPoolSize());
xaDataSource.setMaxLifetime(config.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(config.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(config.getLoginTimeout());
xaDataSource.setMaintenanceInterval(config.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(config.getMaxIdleTime());
xaDataSource.setTestQuery(config.getTestQuery());
return xaDataSource;
}
@Primary
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}

@Primary
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@SpringBootConfiguration
//basePackages 最好分開配置 如果放在同一個文件夾可能會報錯
@MapperScan(basePackages = "com.lynn.demo.test02", sqlSessionTemplateRef = "sqlSessionTemplate2")
public class MyBatisConfig2 {

// 配置數據源
@Bean(name = "dataSource2")
public DataSource dataSource(DBConfig2 config) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(config.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(config.getPassword());
mysqlXaDataSource.setUser(config.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("dataSource2");

xaDataSource.setMinPoolSize(config.getMinPoolSize());
xaDataSource.setMaxPoolSize(config.getMaxPoolSize());
xaDataSource.setMaxLifetime(config.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(config.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(config.getLoginTimeout());
xaDataSource.setMaintenanceInterval(config.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(config.getMaxIdleTime());
xaDataSource.setTestQuery(config.getTestQuery());
return xaDataSource;
}

@Bean(name = "sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}

@Bean(name = "sqlSessionTemplate2")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
在 com.lynn.demo.test01 和 com.lynn.demo.test02 中分別創建以下 mapper:

@Mapper
public interface UserMapper1 {

@Insert("insert into test_user(name,age) values(#{name},#{age})")
void addUser(@Param("name")String name,@Param("age") int age);
}
@Mapper
public interface UserMapper2 {

@Insert("insert into test_user(name,age) values(#{name},#{age})")
void addUser(@Param("name") String name,@Param("age") int age);
}
創建 service 類:

@Service
public class UserService {

@Autowired
private UserMapper1 userMapper1;
@Autowired
private UserMapper2 userMapper2;

@Transactional
public void addUser(User user)throws Exception{
userMapper1.addUser(user.getName(),user.getAge());
userMapper2.addUser(user.getName(),user.getAge());
}
}
5.創建單元測試類進行測試:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class TestDB {

@Autowired
private UserService userService;

@Test
public void test(){
User user = new User();
user.setName("lynn");
user.setAge(10);
try {
userService.addUser(user);
}catch (Exception e){
e.printStackTrace();
}
}
}
經過測試,如果沒有報錯,則數據被分別添加到兩個數據庫表中,如果有報錯,則數據不會增加。

通過前面基礎組件的學習,我們已經可以利用這些組件搭建一個比較完整的微服務架構,為了鞏固我們前面學習的知識,從本文開始,將以一個實際的案例帶領大家構建一個完整的微服務架構。

需求分析
我要實現的一個產品是新聞門戶網站,首先我們需要對其進行需求分析,本新聞門戶網站包括的功能大概有以下幾個:

注冊登錄
新聞列表
用戶評論
產品設計
根據需求分析,就可以進行產品設計,主要是原型設計,我們先看看大致的原型設計圖。

 

 

首頁原型設計圖

 

 

 

文章列表頁原型設計圖

 

 

 

文章詳情頁原型設計圖

 

 

 

個人中心頁原型設計圖

 

 

 

用戶注冊頁原型設計圖

 

 

 

用戶登錄頁原型設計圖

 

數據庫設計
根據原型設計圖,我們可以分析出數據結構,從而設計數據庫:

/*
Navicat Premium Data Transfer
Source Server : 本地
Source Server Type : MySQL
Source Server Version : 50709
Source Host : localhost:3306
Source Schema : news_db
Target Server Type : MySQL
Target Server Version : 50709
File Encoding : 65001
Date: 07/06/2018 21:15:58
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for news_article
-- ----------------------------
DROP TABLE IF EXISTS `news_article`;
CREATE TABLE `news_article` (
`id` bigint(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`gmt_create` datetime DEFAULT NULL COMMENT '創建時間',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改時間',
`title` varchar(64) DEFAULT NULL COMMENT '標題',
`summary` varchar(256) DEFAULT NULL COMMENT '摘要',
`pic_url` varchar(256) DEFAULT NULL COMMENT '圖片',
`view_count` int(8) DEFAULT NULL COMMENT '瀏覽數',
`source` varchar(32) DEFAULT NULL COMMENT '來源',
`content` text COMMENT '文章內容',
`category_id` bigint(16) DEFAULT NULL COMMENT '分類ID',
`is_recommend` tinyint(1) DEFAULT '0' COMMENT '是否推薦',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for news_captcha
-- ----------------------------
DROP TABLE IF EXISTS `news_captcha`;
CREATE TABLE `news_captcha` (
`id` bigint(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
`mobile` varchar(16) DEFAULT NULL COMMENT '手機號',
`code` varchar(8) DEFAULT NULL COMMENT '驗證碼',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for news_category
-- ----------------------------
DROP TABLE IF EXISTS `news_category`;
CREATE TABLE `news_category` (
`id` bigint(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
`name` varchar(16) DEFAULT NULL COMMENT '分類名',
`parent_id` bigint(16) NOT NULL DEFAULT '0' COMMENT '上級分類ID(0為頂級分類)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for news_comment
-- ----------------------------
DROP TABLE IF EXISTS `news_comment`;
CREATE TABLE `news_comment` (
`id` bigint(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
`article_id` bigint(16) DEFAULT NULL COMMENT '文章ID',
`content` varchar(256) DEFAULT NULL COMMENT '評論內容',
`parent_id` bigint(16) NOT NULL DEFAULT '0' COMMENT '上級評論ID(0為頂級評論)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for news_user
-- ----------------------------
DROP TABLE IF EXISTS `news_user`;
CREATE TABLE `news_user` (
`id` bigint(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
`mobile` varchar(16) DEFAULT NULL COMMENT '手機號',
`password` varchar(64) DEFAULT NULL COMMENT '密碼(SHA1加密)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

SET FOREIGN_KEY_CHECKS = 1;
架構圖設計
對於現代微服務架構來說,我們在搭建項目之前最好先設計架構圖,因為微服務工程較多,關系比較復雜,有了架構圖,更有利於我們進行架構設計,下面請看本實例的架構圖:

 

框架搭建
根據架構圖,我們就可以開始搭建框架,首先要進行技術選型,也就是需要集成什么技術,本實例,我們將能夠看到注冊中心、配置中心、服務網關、Redis、MySQL、API 鑒權等技術,下面請看具體代碼。

架構圖截圖:

 

我們知道,微服務架構其實是由多個工程組成的,根據架構圖,我們就可以先把所有工程創建好:

 

其中,common 不是一個項目工程,而是公共類庫,所有項目都依賴它,我們可以把公共代碼放在 common 下,比如字符串的處理、日期處理、Redis 處理、JSON 處理等。

client 包括客戶端工程,config 為配置中心,gateway 為服務網關,register 為注冊中心。

本文我們先來搭建注冊中心、配置中心和服務網關。

1.注冊中心

首先創建啟動類:

package com.lynn.register;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
然后創建 YAML 配置文件:

server:
port: 8888
spring:
application:
name: register
profiles:
active: dev
eureka:
server:
#開啟自我保護
enable-self-preservation: true
instance:
#以IP地址注冊
preferIpAddress: true
hostname: ${spring.cloud.client.ipAddress}
instanceId: ${spring.cloud.client.ipAddress}:${server.port}
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
2.配置中心

創建啟動類:

package com.lynn.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
創建 YAML 配置文件:

server:
port: 8101
spring:
application:
name: config
profiles:
active: dev
cloud:
config:
server:
git:
uri: https://github.com/springcloudlynn/springcloudinactivity #配置git倉庫地址
searchPaths: repo #配置倉庫路徑
username: springcloudlynn #訪問git倉庫的用戶名
password: ly123456 #訪問git倉庫的用戶密碼
label: master #配置倉庫的分支
eureka:
instance:
hostname: ${spring.cloud.client.ipAddress}
instanceId: ${spring.cloud.client.ipAddress}:${server.port}
client:
serviceUrl:
defaultZone: http://localhost:8888/eureka/
3.服務網關

我們繼續編寫服務網關。

首先是啟動類:

package com.lynn.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableEurekaClient
@SpringBootApplication
@EnableZuulProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
服務網關的配置可以通過配置中心拉下來,下面是配置文件代碼,此時配置文件名字為 bootstrap.yml:

spring:
application:
name: gateway
profiles:
active: dev
cloud:
config:
name: gateway,eureka,key
label: master
discovery:
enabled: true
serviceId: config
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8888/eureka/
基礎框架就搭建到這里,后面將繼續搭建基礎框架,謝謝繼續關注。
---------------------
作者:java從菜鳥到菜鳥
來源:CSDN
原文:https://blog.csdn.net/weixin_41446894/article/details/86260854
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM