java 並發基礎,及案例分析


對於我們開發的網站,如果網站的訪問量非常大的話,那么我們就需要考慮相關的並發訪問問題了,然而並發問題是令我們大多數程序員頭疼的問題,但話又說回來了,既然逃避不掉,那我們就坦然面對吧~今天就讓我們深入研究一下常見的並發和同步問題吧。

 

一、同步和異步的區別和聯系

    為了更好的理解同步和並發的問題,我們需要先掌握兩個重要的概念:同步、異步

    同步:可以理解為在執行完一個函數或者方法后,一直等待系統返回值或消息,這時程序是處於阻塞的狀態,只有接收到系統的返回值或者消息后,才會繼續往下執行。

    異步:執行完函數或方法后,不必阻塞性的等待返回值或消息,只需要向系統委托一個異步過程,那么系統接收到返回值或消息時,就會自動觸發委托的異步過程,從而完成一個完整的流程。

 

    同步在一定程度上可以看做是單線程,這個線程請求一個方法后,就等待這個方法給他回復,否則不往下執行(死心眼子)。

    異步在一定程度上可以看做是多線程(廢話,一個線程怎么叫異步),請求一個方法后就不管了,繼續執行接下來的其他方法。

    

    同步就一件事、一件事、一件事的做。

    異步就是做一件事情,不影響做其他的事情。

    例如:吃飯和說話是同步的,只能一件事一件事的來,因為只有一張嘴。吃飯和聽音樂是異步的,聽音樂不影響我們吃飯。

  

  對於java程序員來說,我們經常見到同步關鍵字 synchronized,假如這個同步的監視對象是一個類,當一個對象A在訪問這個類里面的同步方法,此時另外一個對象B也想訪問這個類里面的這個同步方法,就會進入阻塞,只有等待前一個對象執行完該同步方法后當前對象才能夠繼續執行該方法;這就是同步。

  相反,如果方法前沒有同步關鍵字修飾的話,那么不同的對象就可以在同一時間訪問同一個方法,這就是異步。

 

  再補充一下,臟數據和不可重復讀的概念:

  1、臟數據

    臟讀是指:一個事務正在訪問數據,並且對數據進行了修改,而這個修改還有提交到數據庫中,這時另外一個事務也訪問這個數據,然后使用了這個數據。因為這個數據是還沒有提交的數據,那么另外一個事務讀到的數據是臟數據(Dirty Data),臟數據所做的操作可能是不正確的。

  2、不可重復讀

    不可重復讀是指:在一個事務內,多次讀同一條數據。這個事務還沒有結束時,另外一個事務也訪問了該數據,那么,在第一個事務中兩次讀數據之間,由於第二個事務的修改,導致第一個事務兩次讀到數據可能不一樣。這樣就發生了在同一個事務內,兩次讀到的數據是不一樣的,因此稱為不可重復讀。

 

二、如何處理並發和同步

  今天講的如何處理並發和同步問題主要是通過鎖機制去解決。我們需要明白鎖機制有兩個層面:

  第一是代碼層面,如java中的同步鎖,典型的就是同步關鍵字synchronize(還有Lock等)。 感興趣的可以參考: http://www.cnblogs.com/xiohao/p/4151408.html

  第二是數據庫層面上,比較典型的就是悲觀鎖和樂觀鎖,這里重點研究一下悲觀鎖(傳統的物理鎖)和樂觀鎖,這兩個鎖:

  1. 悲觀鎖(Pessimistic Locking)

    悲觀鎖正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及外部系統的事務)的修改持保守狀態,因此,在整個數據處理過程中,將數據處於鎖定狀態。

    悲觀鎖的實現,一般是依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則即使在本系統中實現了加鎖機制,也無法保證外部系統會修改數據)。

    一個典型的依賴數據庫悲觀鎖調用:select * from account where name='zhangsan' for update;

    整條sql鎖定了account表中所有符合檢索條件(name='zhangsan')的記錄;本次事務提交之前(事務提交會釋放事務過程中的鎖),外界無法修改這些記錄。

 

hibernate的悲觀鎖,也是基於數據庫的鎖機制實現的,下面代碼實現了對查詢記錄的加鎖:

1 String hqlStr ="from TUser as user where user.name='zhangsan' ";
2 Query query = session.createQuery(hqlStr);
3 query.setLockMode("user",LockMode.UPGRADE); // 加鎖
4 List userList = query.list();// 執行查詢,獲取數

 

query.setLocalMode 對查詢語句中特定別名所對應的記錄進行加鎖(我們對TUser類制定了一個別名“user”),這也就是對多有返回的記錄加鎖。

  

觀察允許其的hibernate,生成的sql語句:這里hibernate通過使用數據庫的 for update 子句實現了悲觀鎖機制

1 select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update

  

    hibernate的加鎖機制有:

      1、LockMode.NONE:無鎖機制

      2、LockMode.WRITE:hibernate 在 insert 和 update 的時候記錄會自動獲取

      3、LockMode.READ:hibernate在讀取記錄的時候會自動獲取

    以上這三種鎖機制一般由heibernate內部使用,如hibernate為保證 update 過程中對象不會被外界修改,會在 save 方法實現中自動為目標對象加上 WRITE 鎖。

      4、LockMode.UPGRADE:利用數據的 for update 子句加鎖

      5、LockMode.UPGRADE_NOWAIT:Oracle 的特定實現,利用 Oracle 的 for update nowait 子句實現加鎖

    上面這兩種鎖機制是我們應用層較為常用的,加鎖一般通過:Criteria.setLockMode;Query.setLockMode;Session.lock;方法實現。

    需要注意點的是:只有在查詢開始之前(也就是hibernate生成sql之前)設置加鎖,才會真正通過數據的鎖機制進行加鎖處理,否則,數據已通過不包含 for update 子句的 select sql 加載進來,所謂數據的加鎖也就無從談起。

    為了更好的理解 select... for update 的鎖表過程,我們以 mysql 為例,進行研究

    要測試鎖定的狀態,可利用 mysql 的 Command Mode,開兩個視窗來測試:

表的基本結構如下:

表中內容如下:

開啟兩個測試窗口,在其中一個窗口執行 select * from ta for update;

然后再另外一個窗口執行 update 操作:

等到第一個窗口 commit 后:

至此,悲觀鎖的機制,有一些感覺了吧,

    需要注意的是 for update 要放到 mysql 的事務中,即 begin 和 commit 之間,否則不起作用。

    至於鎖住整張表還是鎖住選中的行,請參考:http://www.cnblogs.com/xiohao/p/4385768.html

 

  2、樂觀鎖 (Optimistic Locking):

    相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事物而言,這樣的開銷是無法承受的。

    如一個金融系統當一個操作員讀取用戶數,並在讀出的數據上進行修改操作(如更改用戶賬戶余額),如果采用悲觀鎖機制,也就意味着整個操作過程中(從讀出數據,修改數據,直到提交數據,甚至還包括操作員中途去煮咖啡的時間),數據庫的記錄始終處於加鎖狀態,可以想到,如果面對成百上千個並發,這種情況會導致什么樣的后果。樂觀鎖的機制在一定程度上解決了這個問題。

    樂觀鎖,大多是基於數據版本(version)記錄機制實現。數據版本:即為在數據庫增加一個版本標識。在基於數據庫表的版本解決方案中,一般是通過在數據庫表中增加一個 version 字段來實現。讀取數據的時候將此版本號一起讀出,之后更新時候將此版本號加一。此時,將提交數據的版本信息與數據庫表對應記錄的當前本版信息進行對比,如果提交數據的當前本版號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。對於上面修改用戶信息的例子而言,假設數據庫賬戶信息表中有一個 version 字段,當前值為1,當前賬戶余額字段 balance 為 100;此時操作員A將此數據讀出,並從賬戶余額扣除 50。在操作員 A 操作過程中,操作員 B 也讀去了該用戶的信息,並從賬戶余額中扣除 20。這是操作員 A 完成了修改工作,將數據版本信息加一(version = 2),和賬戶扣除后的余額(balance = 50),提交到數據庫更新,此時由於提交數據版本大於數據庫記錄的版本號,數據記錄被更新 version 更新為 2。接着,操作員 B 完成了操作,也將版本號加一(version = 2),試圖提交數據(balance = 80),但此時對比數據庫記錄版本時候發現,數據庫當前本版也為2,不滿足“數據版本必須大於記錄當前版本才能執行更新”的樂觀鎖策略,因此操作員 B 的提交被駁回。這樣就避免了操作員 B 使用基於 vsersion=1 的舊數據修改的結果覆蓋了操作員 A 的操作結果。從例子可以看出,樂觀鎖的機制避免了長事務中數據庫加鎖數據庫開銷(操作員A和操作員B操作過程中都沒有對數據庫數據加鎖),大大提升了大並發下系統整體性能的表現,來自外部系統的用戶余額更新操作不受我們系統的控制,因此可能會造成在臟數據被更新到數據中,在系統設計階段,我們應該充分考慮到發送這些情況的可能性,並進行相應的調整(如將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基於此存儲過程的數據更新途徑,而不是直接將數據庫表直接對外公開)。

    Hibernate在其數據訪問引擎中內置了樂觀鎖的實現。如果不考慮外部系統對數據庫的更新操作,利用Hibernate 提供的透明化樂觀鎖實現,將大大提升我們的生產力。

User.hbm.xml : 注意 version 節點必須出現在 ID 節點之后。 

 1 <?xml version="1.0"?>
 2 <!DOCTYPE hibernate-mapping PUBLIC
 3         "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 4         "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
 5  
 6 <hibernate-mapping package="com.ayht.test">
 7     <class name="User"  table="user" optimistic-lock="version" >
 8         <id name="id">
 9             <generator class="native" />
10         </id>
11         <!--version標簽必須跟在id標簽后面-->
12         <version column="version" name="version"  />
13         <property name="userName"/>
14         <property name="password"/>    
15     </class> 
16 </hibernate-mapping>

 這里我們申明了一個 version 屬性,用於存放用戶的版本信息,保存在User表的 version 中,optimistic-lock 屬性有如下取值:

  none :無樂觀鎖

  version : 通過版本機制實現樂觀鎖

  dirty : 通過檢查發送變動的屬性實現樂觀鎖

  all :通過檢查所有屬性實現樂觀鎖

 其中通過 version 實現樂觀鎖機制是 Hibernate 官方推薦的樂觀鎖實現,同時也是 Hibernate 中,目前唯一在數據對象脫離Session 發生修改的情況下依然有效的鎖機制。因此一般情況下我們都選擇 version 的方式作為 Hibernate 的樂觀鎖實現機制。

 hibernate.cfg.xml

 1 <!DOCTYPE hibernate-configuration PUBLIC
 2         "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
 3         "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
 4  
 5 <hibernate-configuration>
 6     <session-factory>
 7         <!-- 指定數據庫方言 如果使用jbpm的話,數據庫方言只能是InnoDB-->
 8         <property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
 9         <!-- 根據需要自動創建數據表 -->
10         <property name="hbm2ddl.auto">update</property>
11         <!-- 顯示Hibernate持久化操作所生成的SQL -->
12         <property name="show_sql">true</property>
13         <!-- 將SQL腳本進行格式化后再輸出 -->
14         <property name="format_sql">false</property>
15         <property name="current_session_context_class">thread</property>
16      
17         <!-- 導入映射配置 -->
18         <property name="connection.url">jdbc:mysql:///user</property>
19         <property name="connection.username">root</property>
20         <property name="connection.password">123456</property>
21         <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
22         <mapping resource="com/xiaohao/test/User.hbm.xml" />
23     </session-factory>
24 </hibernate-configuration>

UserTest.java:每次對 TUser 更新的時候,我們會發現,數據庫的 version 都在遞增。

 1 package com.xiaohao.test;
 2  
 3 import org.hibernate.Session;
 4 import org.hibernate.SessionFactory;
 5 import org.hibernate.Transaction;
 6 import org.hibernate.cfg.Configuration;
 7  
 8 public class UserTest {
 9     public static void main(String[] args) {
10         Configuration conf=new Configuration().configure();
11         SessionFactory sf=conf.buildSessionFactory();
12         Session session=sf.getCurrentSession();
13         Transaction tx=session.beginTransaction();
14 //      User user=new User("小浩","英雄");
15 //      session.save(user);
16 //      session.createSQLQuery("insert into user(userName,password) value('張英雄16','123')").executeUpdate();
17         User user=(User) session.get(User.class, 1);
18         user.setUserName("221");
19 //      session.save(user);
20      
21         System.out.println("恭喜您,用戶的數據插入成功了哦~~");
22         tx.commit();
23     }
24 }

 

以下我們將要通過樂觀鎖來實現一下並發和同步的測試用例:我們准備兩個測試類,分別運行在不同的虛擬機上,以此來模擬多個用戶同時操作同一張表,同事一個測試類要模擬一個長事務。

UserTest.java

package com.xiaohao.test;
 
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
 
public class UserTest {
    public static void main(String[] args) {
        Configuration conf=new Configuration().configure();
        SessionFactory sf=conf.buildSessionFactory();
        Session session=sf.openSession();
//      Session session2=sf.openSession();
        User user=(User) session.createQuery(" from User user where user=5").uniqueResult();
//      User user2=(User) session.createQuery(" from User user where user=5").uniqueResult();
        System.out.println(user.getVersion());
//      System.out.println(user2.getVersion());
        Transaction tx=session.beginTransaction();
        user.setUserName("101");
        tx.commit();
        
        System.out.println(user.getVersion());
//      System.out.println(user2.getVersion());
//      System.out.println(user.getVersion()==user2.getVersion());
//      Transaction tx2=session2.beginTransaction();
//      user2.setUserName("4468");
//      tx2.commit();
     
    }
}

UserTest2.java

package com.xiaohao.test;
 
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
 
public class UserTest2 {
    public static void main(String[] args) throws InterruptedException {
        Configuration conf=new Configuration().configure();
        SessionFactory sf=conf.buildSessionFactory();
        Session session=sf.openSession();
//      Session session2=sf.openSession();
        User user=(User) session.createQuery(" from User user where user=5").uniqueResult();
        Thread.sleep(10000);
//      User user2=(User) session.createQuery(" from User user where user=5").uniqueResult();
        System.out.println(user.getVersion());
//      System.out.println(user2.getVersion());
        Transaction tx=session.beginTransaction();
        user.setUserName("100");
        tx.commit();
         
        System.out.println(user.getVersion());
//      System.out.println(user2.getVersion());
//      System.out.println(user.getVersion()==user2.getVersion());
//      Transaction tx2=session2.beginTransaction();
//      user2.setUserName("4468");
//      tx2.commit();
     
    }
}

這里首先啟動 UserTest2.java 測試類,在執行到 Thread.sleep(10000); 這里的時候,當前線程會進入到休眠狀態。在10秒內啟動 UserTest.java 類,在到達10秒的時候,UserTest.java 將會拋出下面的異常:

 1 Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.xiaohao.test.User#5]
 2     at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1932)
 3     at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2576)
 4     at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2476)
 5     at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2803)
 6     at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:113)
 7     at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
 8     at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265)
 9     at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185)
10     at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
11     at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
12     at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
13     at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383)
14     at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133)
15     at com.xiaohao.test.UserTest2.main(UserTest2.java:21)

UserTest.java 將在 tx.commit(); 處拋出:StaleObjectStateException 異常,並指出版本檢查失敗,當前事務正在准備提交一個過期數據,通過捕捉這個異常,我們可以在樂觀鎖效驗失敗時候進行相應的處理。

三、常見並發同步案例分析:

  案例一:訂票系統案例:某航班只有一張機票,假定有1W個人打開你的網站來訂票,問你如何解決並發問題(可擴展到任何高並發網站要考慮到的並發讀寫問題)

  問題1:1W個人來訪問,票沒訂出去前要保證大家都能看到有票,不可能一個人在看到票的時候別人就不能看了。

  問題2:並發1W個人同時點擊購買,總共只有一張票,到底誰能買到。

  首先我們容易想到和並發相關的幾個方案:

  鎖同步更多指的是應用程序層面,多線程進來,只能一個一個的訪問,java 中使用的是 synchronized 關鍵字。鎖也有兩個層面,一個是 java 中談到的對象鎖,用於線程同步;另外一個層面是數據庫鎖;如果是分布式的系統,顯然只能利用數據庫的鎖來實現。

  假定我們采用了同步機制或者數據庫物理機制,如何保證1W個人還能同時看到有票,顯然會犧牲性能,在高並發網站中不可取。使用 Hibernate 之后我們提出了另外一個概念:樂觀鎖、悲觀鎖(傳統的物理鎖)。

  采用樂觀鎖即可解決此問題,樂觀鎖的意思是在不鎖定表的情況下,利用業務的控制來解決並發問題,這樣既保證數據的並發可讀性、又保證保存數據的排他性。保證性能的同時,解決了並發帶來臟數據的問題。

  Hibernate 中實現樂觀鎖的方法:

  注意:在現有表中增加一個冗余字段,version 版本號,long 類型

  原理:1、只有當前版本號 >= 數據庫表版本號,才能提交

     2、提交成功后版本號 version++

  實現很簡單:在 or-mapping 增加一個屬性 optimistic-lock="version" 即可。一下是樣例片段:

1 <hibernate-mapping>
2 
3 <class name="com.xiaohao.abc.ABC" optimistic-lock="version" table="T_Stock" schema="STOCK">

  案例二:股票交易系統、銀行系統,大數據量如何考慮?

  首先股票交易系統的行情表,每幾秒鍾就有一個行情記錄產生,假定行情3秒一條,一天下來就有 股票數量 * 20 * 60 * 6 條記錄,一個月下來這個表數量有多大?數據庫中表的記錄數超過100W后,查詢性能就很差了,如何保證系統性能?

  再比如:中國移動有上億的用戶量,所有用戶都存在一張表嗎?表如何設計?所以大數據量的系統,必須考慮表拆分(表名不一樣,表結構完全一樣),通用的幾種方式:

  1、按業務拆分,比如手機號的表,可以考慮130開頭的號碼存在一張表,131開頭的再存一張表,以此類推。

  2、利用 oracle 的表拆分機制做分表

  3、如果是交易系統,可以考慮按時間軸拆分,當日數據一個表,歷史數據保存到其他表。這里的歷史數據的報表和查詢不會影響當日交易

  當然、表拆分后我們應用程序非做相應的適配,單純的 or-mapping 就得要改動了,比如業務部分得通過存儲過程等。

  此外、我們還要考慮緩存,這個緩存不僅僅指的是 Hibernate 提供的一、二級緩存。這個緩存獨立於應用,依然是內存的讀取,假如我們能減少數據庫的頻繁訪問,那對系統性能肯定大大有利。比如電子商務系統的商品搜索,如果某個關鍵字的商品經常被搜索,那就可以考慮把這部分的商品列表存放到緩存中(內存中),這樣不用每次訪問數據庫,使系統性能大大增加。

  簡單的緩存可以理解為一個 hashMap,把經常訪問的數據作為一個key,value是第一次從數據庫查出的數據,下次訪問就可以從map中取值,而不讀取數據庫。目前可以使用memcached,redis,等。可獨立部署成一個緩存服務器。

  案例三:搶購秒殺業務的解決方案:

  之前我們將高並發解決方案誤認為可以用線程或者隊列解決,因為高並發的時候有很多用戶在訪問,導致出現系統數據不正確,丟失數據現象。所以想到的是用隊列解決。

  其實隊列解決的方式也可以處理,比如我們再競拍商品、轉發評論微博、秒殺商品等,同一時間訪問量特別大,隊列再次起到特別的作用,將所有請求放入隊列,以毫秒為計時單位,有序的進行,從而不回初選數據丟失系統不正確的情況。

四、常見的高並發下提高訪問效率的方法:

  首先了解高並發的瓶頸在哪里

  1、可能是服務器網絡帶寬不夠:可以增加網路帶寬,DNS域名解析分發多台服務器。

  2、可能是web線程連接數不夠:負載均衡,前置代理服務器 negix、Apache 等等。

  3、可能是數據庫連接查詢上不去:數據庫查詢優化、讀寫分離,分表等等。

  以下列出一些在高並發下經常需要處理的內容:

  1、盡量使用緩存,包括用戶信息緩存,常用信息緩存,多花點內存來做緩存,可以大量減少與數據庫的交互,提高性能。

  2、用 jprofile 等工具找出性能瓶頸,減少額外的開銷。

  3、優化數據庫庫查詢語句,減少直接使用 Hibernate 等工具生成查詢語句(只針對耗時長的語句做優化)。

  4、數據庫結構,使用索引,提高查詢效率。

  5、統計的功能盡量作緩存,或按每天一統計或者定時統計相關報表,避免需要時進行統計的功能。

  6、能使用靜態頁面的地方盡量使用靜態頁面,減少容器的解析(盡量將動態的內容生成 html 來顯示)。

  7、解決以上問題之后,盡量使用服務器集群來解決單台服務器的瓶頸。 

  高並發的解決方案有如下兩種:

  第一:使用緩存;第二:生成靜態頁面;

  還有就是從最基礎的地方優化我們寫的代碼,減少不必要的資源浪費:

  1、不要頻繁 new 對象;對於在整個應用中只需要存在一個實例的類,使用單例模式;對於 String 的拼接操作,使用 StringBuffer 或者 StringBuilder;對於 utility 類型的類通過靜態方法來訪問。

  2、避免使用錯誤的方式退出,如 Exception 可以控制方法退出,但是 Exception 要保留 stacktrace 消耗性能;除非必要,不要使用 instanceOf 做條件判斷,盡量使用比的方式做判斷;使用 java 中效率高的類,例如 ArrayList 比 Vector 性能好。

  使用緩存比如 redis ,在應用啟動期間先把用戶數據封裝為 json 格式的字符串,以 hash(哈希)類型加載到 redis 內存中。訪問的時候就可以直接從 redis 中獲取到用戶信息,redis的性能不用質疑是非常好用的,這里就不在多說了。這樣就大大減少了訪問數據庫的頻次,從而提升系統性能。

  生成靜態頁面我想大家應該不陌生,我們見過很多網站在請求的時候頁面地址后綴已經改變了,如:https://www.cnblogs.com/xxx/p/8026575.html,該頁面其實是一個服務器請求地址,再轉換成 html 之后,訪問速度將提升,因為靜態頁面將不帶有服務器組件。請看以下介紹:

  1、什么是生成靜態頁面

    簡單的說,如果我們訪問一個連接,服務器對應的模塊會處理這個請求,轉到對應的 jsp 頁面,最后生成我們想要看到的數據。這其中的缺點是顯而易見的,因為每次請求服務器都會處理,如果有太多高並發處理,那么就會加重服務器的壓力,弄不好就把服務器 down 掉了。那么如何去避免呢,如果我們吧 test.do 請求后的結果保存成一個 html 文件,每次用戶去訪問,會自動生成 test.html 然后顯示給用戶。

  2、下面簡單介紹一下頁面靜態化的知識點:

    1、基礎:URL Rewirte

    URL 重寫:簡單的說:輸入網址,但實際上訪問的是:abc.com/test.action 那么我就可以說 URL重寫了。這項技術應用廣泛,有許多開源工具可以實現這個功能。

    2、基礎:Servlet web.xml 

    如果你還不知道 web.xml 中一個請求和 servlet 是如何匹配到一起的,那么請搜索一下 servlet 文檔。這可不是亂說呀,有很多人認為 /zxy/*.do 這樣的匹配方式能有效。

  3、基本方案介紹:

    請求 index -> URL Rewriter -> 請求 index.action -> 處理 .action 的 servlet -> 如果靜態頁面存在直接返回,否則請求 index.do 到 Struts servlet -> 生成靜態頁面返回。

    

    其中,對於 URL 重寫的部分,可以使用開源的工具來實現,如果 URL 不是特別復雜,可以考慮在 servlet 中實現,那么就是下面這個樣子:

    

    總結:其實在開發中我們很少考慮這種問題,直接都是現將功能實現,當一個程序員干了 1 - 2 年的時候,就會感覺光實現功能不是最主要的,安全性能,可靠性,可用性,擴展性等才是一個開發人員最該關心的。

    今天所說的高並發:我們的解決思路是:1、采用分布式應用設計;2、分布式緩存數據庫;3、代碼優化;

  再來一個 java 高並發的例子:

  具體情況是:通過 java 和數據庫自己實現序列自增:

  id_table 表結構,主要字段:

1 id_name  varchar2(16);
2 id_val  number(16,0);
3 id_prefix  varchar2(4);

  java 代碼大致如下:

 1 //操作DB 
 2 public synchronized String nextStringValue(String id){
 3     SqlSession sqlSess = SqlSessionUtil.getSqlSession();
 4     sqlSess.update("update id_table set id_val = id_val + 1 where id_name="+id);
 5     Map map = sqlSess.getOne("select id_name, id_prefix, id_val from id_table where id_name="+ id);
 6     BigDecimal val = (BigDecimal) map.get("id_val");
 7     // id_val是具體數字,rePack主要是統一返回固定長度的字符串;如:Y0000001, F0000001, T0000001等
 8     String idValue = rePack(val, map); 
 9     return idValue;
10   }
11    
12 //公共方法
13 public class IdHelpTool{
14     public static String getNextStringValue(String idName){
15         return getXX().nextStringValue(idName);
16     }
17 }

具體使用時,都是通過類似這種方式,IdHelpTool.getNextStringValue("PAY_LOG"); 來調用。

  問題:

    1、當出現並發時候,有時會獲取重復的ID。

    2、由於服務器做了一些相關設置,有時候調用這個方法還會導致超時。

  解決思路一:

  1、出現重復ID,是應為臟讀了,並發的時候不加 synchronized 會出現問題

  2、但是加了synchronized ,會導致性能急劇下降,本身 java 就是多線程的,你把它單線程使用,不是明智的選擇。還有如果分布式部署的時候,加了 synchronized 也無法控制並發。

  3、調用這個方法,出現超時,說明你並發已經超過數據庫的處理能力,數據庫無限等待導致超時。

  基於以上分析,建議采用線程池的方案,數據庫 update 不是一次加 1 ,而是一次加幾百甚至上千,然后取到這些序號,放在線程池里慢慢分配,能應付任意大的並發,同時保證數據庫沒有任何壓力。

  如果數據庫可以使用非關系型數據庫,建議使用 redis incy 來實現。具體請參考 redis 文檔。


免責聲明!

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



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