編碼的那麼點事兒

編碼的那麼點事兒

    在這篇文章中很少會去詳細、硬性的去要求要如何寫代碼,更多的是提出一種編碼的時候需要注意的一些情況,和一些更安全,更高效的編碼思路。但是在現有框架下,也會對Service,DAO類有一些硬性的命名要求。
    在redis的使用場景上也會有一些介紹,redis做爲一個分佈式NoSQL數據庫,也是現在比較流行的緩存方案,它可以解決不少問題,但是也有一些問題我們需要去避免。
    希望閱讀本篇文章,能讓你對編碼有新的認識,而不再是copy 和paste。

基礎

  1. 命名和術語

    • 使用英文單詞命名,不用漢語拼音,達到顧名思義
    • 類名首字母大寫,後面單詞的首字母大寫,其餘字母小寫。其次類名要表達類的作用,如Service接口,就像UserService,
      如Service實現類,就像UserServiceImpl,如Dao類,就像UserDao。
    public class MidSchoolClass {
        private class Group {
    
        }
    
        private static class Member {
    
        }
    
    }
    
    public interface UserService{
    
    }
    
    public class UserServiceImpl extends UserService{
    
    }
    
    public interface UserDao{
    
    }
    • 靜態方法,成員方法和成員字段都使用駝峯的方式命名。前面是動詞,後面是名詞,如新增會員:addMember(),如單元測試:testAddMember()。
    private Member oldMember = new Member();
    private int countMembers = 0;
    
    /** 
    * 新增會員
    * @param newMember 新會員
    */
    public void addMember(Member newMember){
    //TODO ....
    }
    
    /**
    * (意思你懂的)
    * @param obj 對象
    */
    public void fuckTheWorld(Object obj){
    //TODO ...
    }
    • 靜態字段或常量使用大寫,單詞之間使用下劃線分開。
    public static final int MEMBER_STATUS_DELETED = 0;
    private static Member MIDDLE_SCHOOL_TEACHER = new Member();
  2. 註釋

    • 類的註釋需要規範:作者、時間、功能介紹等;如果比較複雜的,還需要有示例代碼。
    • 方法級別的註釋需要對方法的功能做詳細介紹,對方法的參數做解釋,介紹返回的內容。如果是實現接口方法或抽象方法可以不用寫註釋,但重寫父類的方法需要寫註釋。
    • 複雜的邏輯部分需要添加註釋。
    • 長方法在多個子代碼塊添加註釋。
    • 對常量字段加註釋描述。
    • 對類或方法進行修改,需要對註釋進行相應的修改,並加上@author 作者。

    看個例子

    import java.lang.annotation.Annotation;
    /**
    * 通過{@code AnnotationHandlerInterceptor}對http請求按照Handler標註的Annotation調用指定的攔截器
    * 攔截器實現這個接口,可以將攔截到的Handler的Controller類和方法上標註的運行時Annotation注入
    * <code>
    * <mvc:interceptors>
    *      <!-- 驗證是否登錄 -->
    *      <mvc:interceptor>
    *          <mvc:mapping path="/**" />
    *          <bean class="com.zzh.college.interceptors.AnnotationHandlerInterceptor">
    *              <property name="annotationTarget">
    *                  <value>TYPE_AND_METHOD</value>
    *              </property>
    *              <property name="annotationClass"
    *                  value="com.zzh.college.request.LoginIntercept" />
    *              <property name="handlerInterceptors">
    *                  <array>
    *                      <bean class="com.zzh.college.interceptors.LoginInterceptor"></bean>
    *                  </array>
    *              </property>
    *          </bean>
    *      </mvc:interceptor>
    * </code>
    * <code>
    * public class LoginInterceptor extends HandlerInterceptorAdapter implements AnnotationInterceptorAware {
    * 
    *  private LoginIntercept loginIntercept;
    * public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    *          throws Exception {
    *      if (loginIntercept.intercept() == InterceptAccess.LOGIN) {
    *          return checkLogin(request, response, handler);
    *      }
    *      return super.preHandle(request, response, handler);
    *  }
    *  
    *      @Override
    *  public void setAnnotation(Annotation annotation) {
    *      loginIntercept = (LoginIntercept) annotation;
    *  }
    * 
    * }
    * </code>
    * 
    * TODO <b>之後會加入泛型擴展</b>
    * @author [email protected]
    * @date 2016.11.16 16:00:00
    *
    */
    public interface AnnotationInterceptorAware {
    
    /**
     * Annotation的注入方法
     * @param annotation
     */
    void setAnnotation(Annotation annotation);
    }

代碼格式化

所有代碼提交至SVN,GIT前,先進行Format

項目小組使用統一格式化風格對代碼進行格式化,這樣便於小組成員間相互的code review;在代碼更新、提交、合併的時候能夠清晰地與老版本代碼對比,查看所修改的代碼。
舉幾個例子

  • 某個小組成員在提交之前將代碼格式化了,到時整個類的風格都變了,在代碼的版本控制系統上與上個版本的代碼進行對比,光想想就腦闊疼了吧。
  • 某個小組成員寫了一行非常長的代碼
//比如com.zzh.platform.vendor.service.impl.ProductServiceImpl#insertProduct()方法
public class ProductServiceImpl implements ProductService {
    //這是一行代碼
    public void insertProduct(ProductInfo prodcutInfo,List<ProductCompanyInfo> productCompanyInfos, long[] categoryIds,List<DictionaryInfo> serviceScopes) throws InterruptedException {
        //TODO ...
    }
}

如果這個方法的throws的異常類改變了,在svn上進行對比時分成兩個頁面顯示,要看到這個變動要移動鼠標,更多人可能會略過;如果有很多行這樣的長代碼改動了,要看其中的改變,也是非常痛苦吧。

  • 在寫代碼的時候還要關心代碼的格式,要讓它便於閱讀,累不累呀?而且其他人還不一定欣賞你的代碼風格。

項目中管理ddl/sql腳本

新建一個source folder,用來管理每個版本的修改調整的數據庫腳本文件。

這裏簡單的介紹一下,我以前在項目中來管理數據庫腳本的方法。
目錄結構如下:

src/main/sql
1.0.0

create_schema.sql
1.0.1
modify_table.sql

每個版本新建一個文件夾,在文件夾中新建sql腳本文件,文件命名也代表着文件內容。
這樣就能夠清淅的看到每個版本中,數據庫的變動。我們開發相互之間可以review,在版本發佈的時候,也可以很快的提交給測試和dba。

代碼書寫

  1. 避免基本性的一些bug

    • 基本類型的包裝類型在參與運算時,要做null值校驗,否則可能出現空指針異常。
    • 基本類型和包裝類型的轉換需要特別注意,如果包裝類型爲null,這時將包裝類型轉換爲基本類型就會拋出空指針異常。尤其是在方法傳參時,包裝類型和基本類型會自動轉換,這時特別需要注意。
    • 使用New 生成的兩個基本類型的包裝類必然不同,通過包裝類的ValueOf生成的包裝類實例可以顯著提高空間和時間性能。
    • 判斷對象是否相等的時候使用equals方法,避免使用“==”產生非預期結果。
  2. 無特殊要求,優先使用基本類型。因爲包裝類被創建後,會在堆中分配內存,非靜態的基本類型的數據是存在方法棧中,使用的是棧的內存,棧的操作速度比堆快很多。

  3. 常量的定義也可以分域,其實有些常量沒有必要放到公共的常量對象中,它可以放很多“地方”,選擇一種讓小組成員很快找到的方式就好。

    • 可以放到接口中,不必放到實現類中。
    • 也可以在某一個模塊的包下創建一個常量對象
    • 也可以在模塊中聲明一個包專門存放enum對象,這些enum對象都是對常量的一種表達
    • 有一種設計叫領域驅動,它有領域對象,領域服務等,領域對象也可以存放常量,而且是最容易查找的。但是在這裏僅僅只是介紹一下有這種設計,目前不推薦使用這種方式。

    在公服平臺,有些常直接定義在zzh-common項目下的某個類中,這樣其實很不可取,因爲zzh-common是一個工具包,更多的時候小組成員使用裏面封裝好的方法就行,而且大部分常量只跟某個特寫的項目有關,甚至只跟項目中的某個模塊有關,在開發的時間,肯定只在模塊中修改代碼更快,而且只對依賴於該應用的項目開放,這樣做之後,zzh-common這個項目,只需要提供jar就行了,也不用去改動zzh-common下的代碼,這個項目更穩定。

  4. 多次使用的相同變量最好歸納成常量,多處使用的相同值的變量應該儘量歸納爲一個常量,方便日後的維護。

  5. 儘量少的在循環中執行方法調用
    儘量在循環中少做一些可避免的方法調用,這樣可以節省方法棧的創建。例如:
    forint i=0;i<list.size();i++){
        System.out.println(i);
    }
    //可以修改爲:
    forint i=0,size=list.size();i<size;i++){
    System.out.println(i);
    }
  1. String,StringBuffer和StringBuilder
    這個問題比較常見。在進行字符串拼接處理的時候,String通常會產生多個對象,而且將多個值緩存到常量池中。例如:

    String a=“a”;
    String b=“b”;
    a=a+b;

    這種情況下jvm會產生“a”,“b”,“ab”三個對象,而且字符串拼接的性能也很低。因此通常需要做字符串處理的時候儘量採用StringBuffer和StringBuilder來,而StringBuffer是使用synchronized關鍵字實現的線程安全的類,看到這是不是都在想:那應該使用StringBuffer啦?其實不然,大部分對字符的操作基本都是封閉在線程內的,根本沒必要去同步,所以我們使用StringBuilder的機會會更多。

  2. 在finally塊中對資源進行釋放
    典型的場景是使用io流的時候,不論是否出現異常最後都應該在finally中對流進行關閉。

    public void readFile(File file){
        InputStream in = null;
        try{
            in = new FileInputStream(file);
            //reading ....
        }catch(IOException e){
            LOG.error(e.getMessage(), e);
        }finally{
            if(in!=null){
                try{
                    in.close();
                }catch(IOException e){
    
                }
            }
        }
    }

    還有是使用java.util.concurrent.Lock的鎖的時候常用來做釋放鎖的操作。

    private ReentrantLock lock = new ReentrantLock();
    public void modifySomeOn(){
        lock.lock();
        try{
            // to do some things...
        }cache(Exception e){
            LOG.error(e.getMessage(), e);
        }finally{
            lock.unlock();
        }
    }
  3. 瞭解apache-commons,使用其提供的封裝好的類。

    對String的一些常用操作,調用StringUtils中的方法去執行,如果沒有的方法,可以自己再去封裝。

    // String equals
    String str = "";
    String str2 = " ";
    boolean isEquals = str.equals(str2);
    boolean isEquals2 = StringUtils.equals(str, "abc");
    
    // String empty
    boolean isEmpty = str==null || "".equals(str); //return true
    boolean isEmpty2 = StringUtils.isEmpty(str);//return true
    boolean isEmpty2 = StringUtils.isBlank(str2); //return true
    
    //String strip
    StringUtils.strip(null, *); // null
    StringUtils.strip("", *); // ""
    StringUtils.strip("abc", null); // "abc"
    StringUtils.strip("  abc", null); // "abc"
    StringUtils.strip("abc  ", null); // "abc"
    StringUtils.strip(" abc ", null); // "abc"
    StringUtils.strip("  abcyx", "xyz"); // "  abc"
    StringUtils.strip("aefabcdfea", "ef"); //會返回什麼結果?
    
    //去除全角的空格
    StringUtils.strip(" jf  ");//"jf"

    對日期的常用操作可以先去了解DateUtils

    Date date = new Date();
    DateUtils.addYears(date, 1);
    DateUtils.addMonths(date, 1);
    DateUtils.addWeeks(date, 1);
    DateUtils.addDays(date, 1);
    DateUtils.addHours(date, 1);
    DateUtils.addMinutes(date, 1);
    DateUtils.addSeconds(date, 1);
    DateUtils.addMilliseconds(date, 1);
    DateUtils.add(date, Calendar.HOUR, 1);
    DateUtils.setYears(date, 1);
    DateUtils.setMonths(date, 1);
    DateUtils.setWeeks(date, 1);
    DateUtils.setDays(date, 1);
    DateUtils.setHours(date, 1);
    DateUtils.setMinutes(date, 1);
    DateUtils.setSeconds(date, 1);
    DateUtils.setMilliseconds(date, 1);
    DateUtils.set(date, Calendar.HOUR, 1);

    如果沒有的方法,可以去建個DateUtils,繼承org.apache.commons.lang.StringUtils.DateUtils再去擴展。

    對File的基本操作可以使用commons-io.jar中的org.apache.commons.io.FileUils。

    // get a file
    File file = FileUtils.getFile("d:/a", "b", "c", "d.txt");//file ==> d:/a/b/c/d.txt
    
    //copy file
    File src = new File("d:/a/b.txt");
    File target = new File("e:/a/b.txt");
    FileUtils.copyFile(src, target);
    
    //copy directory
    File srcDir = new File("d:/a");
    File targetDir = new File("d:/b");
    FileUtils.copyDirectory(srcDir, targetDir);
    
    //get input or output stream
    File file = new File("d:/a.txt");
    OutputStream out = FileUtils.openOutputStream(file);
    InputStream in = FileUtils.openInputStream(file);
    
    //read file string content
    File file = new File("d:/a.txt");
    String content = FileUtils.readFileToString(file, Charset.forName("UTF-8"));
    
    //write content to a file
    File file = new File("d:/a.txt");
    FileUtils.writeStringToFile(file, "content", Charset.forName("UTF-8"));
    FileUtils.writeByteArrayToFile(file, "content2".getBytes(Charset.forName("UTF-8")));

    如果沒有的方法,可以新建個FileUtils,繼承org.apache.commons.io.FileUtils再去擴展。
    還有apache-commons項目下還有很其它的工具類,都可以去了解,並去封裝擴展,比如對io進行操作的org.apache.commons.io.IOUtils, 對Bean屬性進行操作的org.apache.commons.beanutils.PropertyUtils,對Bean進行操作的org.apache.commons.beanutils.BeanUtils和org.springframework.beans.BeanUtils,對基本類型進行轉換的org.apache.commons.beanutils.ConvertUtils等等,這裏指出這些,只是提出這樣這種思想:使用一些開源組織封裝好的工具類去做一些事,不夠的,可以再去擴展封裝。

  4. 注意數據邊界以及數據安全的校驗
    瞭解數據邊界,對於編碼的效率會有很大的提高,爲什麼呢?因爲你不用思考一些判斷我該放在哪兒呢?這裏是否需要進行判斷呢?那麼瞭解了數據邊界,就知道該在那裏進行判斷,編碼速度提高了,代碼性能提高了,程序的穩定性也提高了。可是數據邊界怎麼去了解?我們將程序分開應用層,服務層,數據層,就是爲了解決這些問題,應用層做爲跟前端接觸的那一層,有些null判斷直接在應用層判斷了,服務層根據應用層傳遞過來的數據再寫業務,將數據提交給數據層持久化。這樣說可能太抽象,還是以例子闡述吧。

    新增用戶

    1. 應用層的UserController接收前端的請求,對數據進行驗證。
    @Controller
    @RequestMapping("/user")
    public UserController {
    
    private static Logger LOG =     LoggerFactory.getLogger(UserController.class);
    
    @Autowired
    private UserService userService;
    
    /** 
     * 添加一個會員
     * @param username
     * @param password
     * @param rePassword
     * @return apiresult json
     */ 
    @RequestMapper("addMember")
    @ResponseBody
    public ApiResult addMember(String username, String password, String rePassword){
        //在handler方法中進行基本的驗證
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            return new ApiResult(-1, "用戶名和密碼不能爲空!");
        }else if(StringUtils.equals(password, rePassword)){
            return new ApiResult(-1, "兩次密碼輸入須一致!");
        }
    
        try{
            Member member = userService.addMember(username, password);
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("id", member.getId());
            map.put("username", member.getUsername());
            return new ApiResult(0, map);
        }catch(Exception e){
            LOG.error(e.getMessage(), e);
            return new ApiResult(-1, "添加會員失敗,請重試!");
        }
    }
    }
    
    
    
    
    
    public class UserService {
    
    /** 
     * 添加一個會員
     * @param username
     * @param password
     * @return 
     * @throws RuntimeException 如果添加失敗,將會拋出異常
     */ 
    public Member addMember(String username, String password) throws RuntimeException;
    
    }
    
    
    
    @Service
    public class UserServiceImpl implements UserService {
    
     @Autowired
     private UserDao userDao;
    
     @Override
     public Member addMember(String username, String password) throws RuntimeException {
        if(StringUtils.length(username)<1 || Stringutils.length(username)>30){
            throw new RuntimeException("用戶名的長度必須在1-30位之間");
        }else if(StringUtils.length(password)<6){
            throw new RuntimeException("密碼不能小於6位");
        } 
        Member member = new Member();
        member.setUsername(username);
        member.setPassword(StringUtils.encrypt(password));
        userDao.insert(member);
     }
    }

    這樣的代碼是不是很清淅呢,因爲判斷邊界很清楚,應用層做一些基礎的判斷,保證數據的正確性提交給服務層,服務層做一些業務的判斷,提交給數據層,這樣就是避免了在寫代碼的時候,還要去思考,是否要做非空判斷,數據長度的判斷啊等等,也不會出現應用層寫一遍判斷,服務層再去寫一遍判斷。

  5. 編寫網絡io操作的時候,一定要考慮到響應速度,設置合理的超時時間。
    如查在編碼的時候沒有考慮到響應速度,設置超時時間,會產生哪些影響呢?
    發生網絡抖動的時候,請求一直得不到目標服務器的響應,會阻塞在那,那麼會這麼幾情況

    • 前端也會相應等待。
    • 線程會等待。
    • 如果持有鎖,鎖不會被釋放。
    • 如果有事務,數據庫連接不會被釋放。
  6. 消耗cpu和io資源的數據,要考慮使用緩存。
    緩存有一句話是這樣來描述它的,“空間換時間”,所以緩存的優勢是快速讀寫,但是它會消耗內存,所以緩存的數據,需要分析其熱點數據,並且是消耗cpu和io資源的數據。
    在java中可以使用SoftReference或WeakReference來實現內存敏感的高速緩存。使用軟引用能防止內存泄露,增強程序的健壯性。
    對於引用有四種:

    • 強引用

      強引用的“強”是取決於它如何處理與GC的關係的:它是無論如何都不會被回收的。

    • 軟引用

      軟引用(SoftReference)則類似於可有可無的東西。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。

    • 弱引用

      弱引用(WeakReference)也類似於可有可無的東西。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,從gcroot處沒有查找其它的引用,不管當前內存空間足夠與否,都會回收它的內存。

    • 虛引用
      虛引用(PhantomReference)這個類型的引用無可奉告,因爲我也不知道。 - -

    看代碼和輸出吧。

        SoftReference<Cache> soft = new SoftReference<Cache>(new Cache(1));
        WeakReference<Cache> weak = new WeakReference<Cache>(new Cache(3));
        PhantomReference<Cache> phantom = new PhantomReference<Cache>(new Cache(4), queue);
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null
    
        System.gc();
        try {
            Thread.sleep(30 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        System.out.println("===after gc==");//===after gc==
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> null
        System.out.println("phantom --> " + phantom.get());//phantom --> null
        SoftReference<Cache> soft = new SoftReference<Cache>(new Cache(1));
        Cache cache = new Cache(3);
        WeakReference<Cache> weak = new WeakReference<Cache>(cache);
        PhantomReference<Cache> phantom = new PhantomReference<Cache>(new Cache(4), queue);
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null
    
        System.gc();
        try {
            Thread.sleep(30 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        System.out.println("===after gc==");//===after gc==
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null

    可以看到上面兩段代碼中WeakReference的引用在gc前後輸出是不一樣的,這是因爲在第二段代碼中,WeakReference引用了變量cache,cache還存在方法棧被引用到,所以還沒有被gc回收。

    上面只是介紹了避免因緩存而導致jvm內存溢出的技術,還可以通過算法對緩存進行管理,比如LRU算法,FIFO算法等,感興趣的猿可以瞭解瞭解,然後結合下面介紹線程安全的節章設計一個線程安全的緩存工具來。

  7. 線程安全
    線程安全?好高大上酷拽吊!但是其實一開始每個人寫的第一個程序就是線程安全的程序,輸出“hello world”,沒錯,這就是一個線程安全的程序,它只有一個main線程,沒有其它線程跟main線程發生竟爭,它能保證輸出”hello world”,它就是線程安全的。
    線程和併發幾乎總是會在一起被談論,這是必然的,因爲進程實操作系統的併發,jvm線程實現了java程序的併發,所以呢,想寫出好的併發程序,那麼你必須瞭解線程,以及它的同步機制,大家可以單獨去了解,這裏只講講編寫線程安全的類的思路,也會帶一點點併發方面的考慮。

    • 一個只有不可變基礎類型屬性(字段)的類,它一定是線程安全的。
    public class ApiRequest{
        private final String version;
        private final long userid;
    
        public ApiRequest(String version, long userId){
            this.version = version;
            this.userId = userId;
        }
    
        public String getVersion(){
            return version;
        }
    
        public long getUserId(){
            return version;
        }
    }

    需要注意的是,一個包含不可變的引用類型屬性的對象,並不能保證足夠的安全,除非那個引用類型也只包含不可變的基礎類型的屬性。看下面的代碼

    public class ApiRequest{
        private final String version;
        private final long userid;
        private final LoginUser user;
    
        public ApiRequest(String version, long userId, LoginUser user){
            this.version = version;
            this.userId = userId;
            this.user = user;
        }
    
        //getter methods ....
    }
    
    public class LoginUser {
        private String username;
        private String password;
    
        public LoginUser(String username, String password){
            this.username = username;
            this.password = password;
        }
    
        //getter/setter methods ....
    }

    上面代碼中ApiRequest對象並不是線程安全的,爲什麼呢?因爲LoginUser不是線程安全的。如果LoginUser修改按下面的代碼那樣修改一下就是了。

    public class LoginUser {
        private final String username;
        private final String password;
    
        public LoginUser(String username, String password){
            this.username = username;
            this.password = password;
        }
    
        //getter methods ....
    }
    • 封閉在線程內的對象,並且沒有對其它線程發佈的對象,它一定是線程安全的。
    @Controller
    @RequestMapping("user")
    public class UserController{
    
        @Autowired
        private UserService userService;
    
        public ApiResult addMember(HttpServletRequest request){
            Member member = new Member();
            member.setUsername(request.getParameter("username"));
            member.setAge(request.getParameter("age"));
    
            //只要後面的代碼不將member對象發佈到其它線程,那麼member對象就是一個線程安全的對象。
            userService.addMember(member);
    
        }
    }

    因爲一個線程會被分配一個棧空間,比如member對象,沒有對這個線程之外的範圍進行發佈,也就是在這個線程的棧空間之外沒有這個對象的引用,那麼其它線程自然沒辦去對它進行訪問。把一個變量發佈出去,最直接的方法就是賦予靜態域,或發佈到靜態域對象的引用中。

    • 使用ThreadLocal對象,將對象封閉到線程內,從而實現程序安全。
    public class AInterceptor extends HandlerInterceptorAdapter {
        @Autowired
        private MemberService memberService;
        private ThreadLocal<Member> memberLocal = new ThreadLocal(); 
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
            long memberId = StringUtils.defaultIfEmpty(request.getParameter("memberId"), "0");
            Member member = memberService.findById(memberId);
            //這時就可以通過memberLocal對外發布member對象了。
            memberLocal.set(member);
            return super.preHandle(request, response, handler);
        }
    
        /** 
         * 這就是一個線程安全的方法
         * @return
         */
        public Member getCurrentMember (){
            return memberLocal.get();
        }
    
        @Override
        public void afterCompletion(
                HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                memberLocal.remove();
        }
    }
    

    ThreadLocal爲什麼能實現線程安全呢,因它他是個Map,Key就是當前線程 –> Thread.currentThread(),有興趣的同學可以去了解一下。

    • 使用鎖將需要同步的代碼變成串行執行。
      java中鎖有兩種,synchronized和ReentrantLock。這兩種鎖最大的區別不是性能上的區別,而且是它們底層實現上的區別。

      1. synchronized是jvm級別的鎖,由jvm管理,它鎖的是對象,如果已有其它線程持有鎖,那麼當前線程就會一直阻塞等待,至到持有鎖的線程執行的代碼退出synchronized代碼塊釋放掉這個對象的鎖,比如:

            public class Test(){
                private byte[] lock = new byte[0];
                public static synchronized void staticLock(){
                    //這個方法鎖的是Test.class
                }
        
                publi synchronized void lockThis(){
                    //這個方法鎖的是this
                }
        
                publc void lockObj(){
                    synchronized(lock){
                    //這段代碼鎖的是數組對象lock
                    }
                }
            }
      2. ReentrantLock是通過無阻塞算法實現,使用的技術是cas(compare and set,有興趣可以去看看AbstractQueuedSynchronizer.acquireQueued(final Node node, int arg)的代碼,下面會有介紹到)。

        public class Test{
        private ReentrantLock lock = new ReentrantLock();
        
        private void lock(){
            if(lock.tryLock(200, TimeUnit.MILLISECONDS)){
                try{
                    // to do some things ...
                }catch(Exception e){
        
                }finally{
                    lock.unlock();
                }
            }
        }
        }
        
      3. 兩種的性能對比
        在jdk1.6以前,是ReentrantLock性能要佔優勢,但是在jdk1.6中,sun對synchronized進行了優化,性能上已經漸漸趕上來了,而且jdk在以後可能會繼續對synchronized進行優化。其實大部份情況下,synchronized已經能夠滿足大部分使用場景。如果需要使用更靈活的併發控制(ReentrantLock提供了newCondition()方法創建一個Condition對象),使用ReentrantLock會更合適些。但是在併發非常激烈的時候,但是又可以不用考慮超時的場景,那更合適使用synchronied,爲什麼?大家想像一下非阻塞算法的場景,再結合對線程的瞭解,就可以知道了。

      4. Condition提供的方法

        //阻塞線程,
        await()
        //阻塞線程,如果超時才退出該方法,返回false,否則true
        boolean awaitUntil(Date deadline)

        //阻塞線程,如果超時才退出該方法,返回false,否則true
        await(long time, TimeUnit unit)

        //喚醒單個線程
        signal()

        //喚醒所有線程
        signalAll()

    • 使用cas非阻塞的算法提供線程安全。
      看看AbstractQueuedSynchronizer中的自旋獲取鎖的那一段代碼。
      判斷當前節點的前驅節點。需要獲取當前節點的前驅節點的狀態,當前驅節點是頭結點並且當前節點(線程)能夠獲取狀態(tryAcquire方法成功),代表該當前節點佔有鎖,如果滿足上述條件,那麼代表能夠佔有鎖,根據節點對鎖佔有的含義,設置頭結點爲當前節點(setHead)。如果沒有滿足上述條件,調用parkAndCheckInterrupt方法使得當前線程disabled,直到unpark調用或者Thread的interrupt調用,然後重新輪訓去嘗試上述操作。

          final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;
          try {
              boolean interrupted = false;
              for (;;) {
                  final Node p = node.predecessor();
                  if (p == head && tryAcquire(arg)) {
                      setHead(node);
                      p.next = null; // help GC
                      failed = false;
                      return interrupted;
                  }
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          int ws = pred.waitStatus;
          if (ws == Node.SIGNAL)
              /*
               * This node has already set status asking a release
               * to signal it, so it can safely park.
               */
              return true;
          if (ws > 0) {
              /*
               * Predecessor was cancelled. Skip over predecessors and
               * indicate retry.
               */
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              pred.next = node;
          } else {
              /*
               * waitStatus must be 0 or PROPAGATE.  Indicate that we
               * need a signal, but don't park yet.  Caller will need to
               * retry to make sure it cannot acquire before parking.
               */
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }
    • 使用已經有的線程安全類去實現同步,從而提供線程安全。
      總結來說:線程安全就是供數據訪問保護,出現多個線程先後更改數據造成所得到的數據不會出現數據不一致或數據污染,線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。
      在某些併發場景下,可以使用已經有的線程安全的類
      concurrent包有:ConcurrentHashMap, CopyOnWriteArrayList,CopyOnWriteArraySet,CountDownLatch,AtomicInteger,AtomicIntegerArray,AtomicReference,AtomicReferenceArray等等。

      synchronized:
      Collections.synchronizedList(list);
      Collections.synchronizedCollection(c);
      Collections.synchronizedMap(m);
      Collections.synchronizedSet(s);
      Collections.synchronizedSortedMap(m);
      Collections.synchronizedSortedSet(s);

  8. 項目配置文件規範
    配置文件一般在開發的時候也會常去修改,並且隨着項目的推進,配置文件也可能會越來越多,當配置文件多的時候,一眼望去,眼睛都花了,所以就需要對配置文件進行合理的管理。
    {profile}表示運行環境:開發環境,測試環境,預發環境,生產環境

    • 項目的properties配置文件

      1. 文件放到src/main/resources/{profile}/conf目錄下
      2. 命名規則全小寫字母,單詞之間用中槓線(-)分開,如system-config.properties。
    • spring xml配置文件

      1. 文件到src/main/resources/spring/context目錄下
      2. 命名規則全小寫字母,單詞之單用中槓線(-)分開,如spring-context.xml
    • hessian xml配置文件

      1. 文件放到src/main/resources/hessian目錄下
      2. 命名規則全小寫字母,單詞之間用中槓線(-)分開,如hessian.xml,hessian-spring.xml
    • summercool-spring xml配置文件

      1. 文件放到src/main/resources/summercool/spring目錄下
      2. 命名規則全小寫字母,單詞之間用中槓線(-)分開,user-web.xml
    • spring mvc xml配置文件

      1. 文件放src/main/resources/spring目錄下
      2. 命名爲spring-mvc.xml
    • log 的配置文件

      1. proper文件放在src/main/resources/{profile}目錄下
      2. 如log4j.properties,log4j.xml,logback.xml
    • mybatis xml映射文件

      1. 文件放在src/main/resources/mapper目錄下
      2. 命名規則全小寫字母,單詞之間用中槓線(-)分開,“zzh-*-mybatis-mapper.xml”,後面能夠看出表名,如:zzh-demand-mybatis-mapper.xml,zzh-demand-dynaimc-mybatis-mapper.xml

      目的是能夠快速查找,並且模塊分開。

  9. log日誌
    log日誌是我們分析線上運行情況的重要依據,我們可以從日誌中分析出哪些接口被調用的多,哪些接口被調用的不正常,還有出現異常時我們可以通過日誌查看到當時運行方法棧信息,和出現異常的原因。當然,這些都要程序猿在編碼的時候編寫好,所以看看下面的編寫吧。
    生產環境

    • 記錄的內容
      1. 生產環境輸出info,warn,error級別的日誌。
      2. info 輸出一些重要的信息。
      3. warn 輸出警告。
      4. error 輸出錯誤,異常。錯誤信息須描述清楚是什麼原因。
    • log文件按小時折分。
    • log文件存放在web容器默認的logs文件夾下。

    開發環境
    開發環境相對來說要打印的日誌就多了,除了上面那些內容,還有就是要打印一些debug信息,方便在開發的調度,能夠快速定位問題點。

  10. 避免xss攻擊
    xss翻譯過來就是跨域腳本攻擊,避免這種攻擊主要的方法就是將html中的<|>|&|”等特殊敏感的字符改爲實體引用。目前已經有代碼實現了這個功能,我們只要拿來用就行了。

    1. 主要用到commons-lang3-3.1.jar這個包的org.apache.commons.lang3.StringEscapeUtils.escapeHtml4()這個方法。
      在web.xml加一個filter
    <filter>
        <filter-name>XssEscape</filter-name>
        <filter-class>cn.pconline.morden.filter.XssFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>XssEscape</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    XssFilter 的實現方式是實現servlet的Filter接口

    package cn.pconline.morden.filter;
    
    import java.io.IOException;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    public class XssFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
        }
    
        @Override
        public void destroy() {
        }
    }

    XssHttpServletRequestWrapper 的代碼

    public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper{
    
    
    
    
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    
    
    @Override    
    public String getHeader(String name) {    
        return StringEscapeUtils.escapeHtml4(super.getHeader(name));    
    }    
    
    @Override    
    public String getQueryString() {    
        return StringEscapeUtils.escapeHtml4(super.getQueryString());    
    }    
    
    @Override    
    public String getParameter(String name) {    
        return StringEscapeUtils.escapeHtml4(super.getParameter(name));    
    }    
    
    @Override    
    public String[] getParameterValues(String name) {    
        String[] values = super.getParameterValues(name);    
        if(values != null) {    
            int length = values.length;    
            String[] escapseValues = new String[length];    
            for(int i = 0; i < length; i++){    
                escapseValues[i] = StringEscapeUtils.escapeHtml4(values[i]);    
            }    
            return escapseValues;    
        }    
        return super.getParameterValues(name);    
    }  
    
    }

    到此爲止,在輸入的過濾就完成了。

    另外,有些情況不想顯示過濾後內容的話,可以用StringEscapeUtils.unescapeHtml4()這個方法,把StringEscapeUtils.escapeHtml4()轉義之後的字符恢復原樣。

  11. 避免sql注入
    java防SQL注入的已經有了PreparedStatement預編譯語,它內置了處理SQL注入的能力,只要使用它的setXXX方法傳值即可,大部分的SQL注入已經擋住了, 在WEB層我們可以過濾用戶的輸入來防止SQL注入比如用Filter來過濾全局的表單參數。
    在web.xml中添加一個filter

    <filter>
        <filter-name>sqlFilter</filter-name>
        <filter-class>com.filter.SQLFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>sqlFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    獻上java代碼
    SQLFilter

    import java.io.IOException;
    import java.util.Iterator;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang3.StringUtils;
    
    /**
     * 通過Filter過濾器來防SQL注入攻擊
     * 
     */
    public class SQLFilter implements Filter {
        private String[][] keyValues = { { "'", "‘" }, { ";", ";" } };
        protected FilterConfig filterConfig = null;
        /**
         * Should a character encoding specified by the client be ignored?
         */
        protected boolean ignore = true;
    
        public void init(FilterConfig config) throws ServletException {
            this.filterConfig = config;
            // key:value|key:value|key:value
            String keywords = filterConfig.getInitParameter("keywords");
            if (StringUtils.isNotEmpty(keywords)) {
                String[] arys = keywords.split("|");
                keyValues = new String[arys.length][2];
                for (int i = 0, size = arys.length; i < size; i++) {
                    String[] words = arys[i].split(":");
                    if (words.length == 2) {
                        keyValues[i] = words;
                    }
                }
            }
        }
    
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
            chain.doFilter(new SqlHttpServletRequestWrapper((HttpServletRequest) request, keyValues), response);
        }
    }

    SqlHttpServletRequestWrapper


import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class SqlHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private Map<String, String[]> parameters;
    private String[][] keyValues;

    public SqlHttpServletRequestWrapper(HttpServletRequest request, String[][] keyValues) {
        super(request);
        this.keyValues = keyValues;
        initParameters();
    }

    @Override
    public String getParameter(String name) {
        String[] strs = parameters.get(name);
        if (strs == null || strs.length == 0) {
            return null;
        }
        return strs[0];
    }

    @Override
    public Map getParameterMap() {
        return Collections.unmodifiableMap(this.parameters);
    }

    @Override
    public String[] getParameterValues(String name) {
        return this.parameters.get(name);
    }

    private void initParameters(){
        Map<String, String[]> params = (Map<String, String[]>) super.getParameterMap();
        Iterator<String> iter = params.keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String[] valueAry = (String[]) params.get(key);
            String[] values = new String[valueAry.length];
            for (int i = 0; i < valueAry.length; i++) {
                values[i] = replaceSqlKey(valueAry[i]);
            }
            parameters.put(key, values);
        }
    }


    public String replaceSqlKey(String str) {
        String retStr = str;
        for (int i = 0, size=keyValues.length; i < size; i++) {
            String [] words = keyValues[i];
            if (str.indexOf(words[0]) >= 0) {
                retStr = retStr.replaceAll(words[0], words[1]);
            }
        }
        return retStr;
    }

自測

我們爲什麼要自測,自測的目的是什麼呢?在回答這個問題前,我們先來了解一下程序開發過程中,發生的一些問題,以及我們該如何去避免。
在編碼結束後,測試部門進行測試的時候,發現程序的流程沒法連貫起來,甚至直接報錯,無法進行後面的測試工作。程序員自測可以減少這部分的問題,至少能保證代碼是可以正常運行的,開發人員自已發現的問題,解決起來是最快的,沒有跟其它人的溝通成本;而且開發人員在自測過程中發現的一些問題,解決後,可以自己總結,從而避免在以後的開發過程中發生類似的情況。
我們一般認爲研發人員對自己開發的模塊進行自測,是應該的,用研發術語來講是默認的,不需要另行強調。程序猿的工作是團隊協作中的一環,應該對自己所做的工作負責。
專業的測試人員也是不希望拿到漏洞百出的程序的。所以,最終,有些問題,我們是繞不開的,那就是提高程序猿的基本職業素養。

我在網上找了兩個例子說明自測的重要性:
編程規範(自測的重要性)http://m.blog.csdn.net/article/details?id=7296972
程序猿自測的重要性(職業素養必備)http://blog.csdn.net/shuaihj/article/details/43272153

數據庫設計

數據庫這塊有些人可能認爲如果公司有dba了,那麼我還去了解數據庫設計幹嘛?但是做後端的開發就是這麼心累,因爲你是做爲後端業務開發,你需要考慮到數據結構是否滿足業務需求,性能需求,以及是否能滿足以後可預見的擴展,不然,一旦數據結構發生很大的變化,那麼業務層的代碼也會需要做很大的調整,所以做爲後端開發,你還得有良好的溝通和協調能力,跟產品設計人員和dba進行討論,得出一個較好的思路或方案。

  1. 數據庫數據邏輯刪除
    對於開發的程序系統來說,數據庫就是存放數據的地方,如果我們從數據庫中將數據使用delete語句將數據物理刪除掉了,那麼有可能那條數據就是永遠找不回來了,想想,如果在寫delete功能的時候,將其中的delete語句的where條件少加了一個發佈到線上,這將是多可怕的事情。

    刪除這個操作對於用戶來說就是不可見,那麼在設計刪除功能的時候,可以加一個刪除標記來標誌數據對用戶來說是否可見,比如字段:del_flag,這樣我們就使用update語句去操作,這樣數據的安全性也會提升很多,而且如果數據被錯誤“刪除”或用戶希望把數據找回來,對於系統依然還是可以查詢得到。

  2. 主鍵(id)類型的字段風格統一
    在這裏我要說說爲什麼是主鍵風格而不是字段類型,因爲在主鍵裏面,它涉及到數據類型,字段大小,以及id是否自增長等等。

    1. 主鍵id的風格統一有什麼好處呢?首先對於其它他通過引用id關聯別一張的數據,那麼在設計的時候,就知道這個關聯id只需要設置成主鍵風格的字段就好了。
    2. mybatis生成empty類,dao接口,mapper.xml配置文件的主鍵id字段和關聯字段風格統一,在java代碼中的對對應的方法和參數的訪問都不需要特別去注間,也不需要特意的去轉換。
  3. 表公共字段

    create_time, update_time, del_flag
    create_time 創建時間
    update_time 更新時間
    del_flag 刪除標記(1:刪除,0:否)
    這三個字段並不是說要求每張表都需要去創建,而是看業務是否需要,比如說,有刪除需求或業務的數據,那麼就可以創建del_flag字段。如果在分析中需要用到數據的是創建時間,或生成時間,那麼可以使用create_time字段,這個字段應該也是大部分表都會創建的。另外就是update_time這個字段,同樣是需求驅動,如果一條數據在以後會執行update操作,那麼就可以創建update_time這個字段。

  4. 適當的使用冗餘字段避免多表查詢
    在mysql中,不太可能會去寫幾百行的sql查詢,而且多表查詢對於mysql來說,會消耗更多的資源,那麼會導致sql查詢慢,影響程序對請求的響應時間,影響程序的併發量,但是適當添加冗餘字段和關聯表,可以避免多表關聯查詢,從而解決前面的問題。

    • 舉個冗餘字段的例子

    地區表area

    字段名 描述
    id
    pid 上級id
    area_name 地區名稱

    學校表school

    字段名 描述
    id
    area_id 地區Id
    school_name 學校名稱

    如果在列表中需要顯示school_name,area_name,那麼就是需要關聯查詢了:

    select a.school_name, b.area_name form school a join area b on a.area_id=b.id

    但是其實只要在school表中添加一個area_name字段,就可以避免這種關聯查詢了呀。
    學校表school

    字段名 描述
    id
    area_id 地區Id
    school_name 學校名稱
    area_name 地區名稱(冗餘字段)
    select school_name, area_name form school 
  5. sql where 條件注意事項

    1. 避免檢索不需要的列
      在檢索記錄且需要顯示的列較多時,儘量不要使用“*”來代替所有列,這樣不僅增加處理時間(MySQL在解析的過程中,會將“*”依次轉換成所有的列名,這個工作是通過查詢數據字典完成的, 這意味着將耗費更多的時間,同時也增加了I/O的量),而且當表結構變化時,原來的列順序有可能完全改變而導致不必要的bug或修改。
    2. 避免檢索列爲null
      避免在WHERE謂詞中包含IS NULL、IS NOT NULL的篩選條件。因爲索引的創建是對應具體值而存在,既然爲NULL也就沒有對應的值,索引自然就會失去索引功能,從而性能會大幅下降。
    3. 避免使用in,not in;使用exists代替in。
      在子查詢中使用了NOT IN演算後,會發生內部排序、合併處理,爲了提高性能,可以用NOT EXISTS來代替NOT IN + 子查詢。但並非所有情況都適用。具體要依據實測結果而定。
    4. 避免使用子查詢和exists,用表連接代替。
    5. 避免多表關聯查詢,必要的時候建冗餘字段,建中間表。
    6. 用UNION ALL代替UNION
      • Union 對兩個結果集進行並集操作,重複數據只顯示一次,存在排序操作。
      • Union All對兩個結果集進行並集操作,重複數據全部顯示。不存在排序操作。
    7. 模糊查詢

      模糊查詢主要有以下三種形式:

      後模糊:col like ‘ABC%’
      前模糊:col like ‘%ABC’
      全模糊:col like ‘%ABC%’

      • 後模糊是最好的,在col字段上建立索引是可以被優化器選擇的,並且是效率比較高的索引範圍掃描方式,所以要儘量採取或轉換成這種形式。
      • 前模糊形式即使在col字段上建立索引通常也還是不會被使用,即使使用效率也不會太高。所以這種方式是要儘量避免的,或者採取一些變通的手段比如採取反轉函數等,但基本都需要改寫原來的代碼。
      • 全模糊形式普通索引的效率也會很差,寫法上也是需要儘量避免的。雖然可以採用全文索引的方式來達到提高索引效率的目的,但全文索引相對複雜且佔用空間要比普通索引大很多,且索引維護時要消耗更多資源,設計上需要綜合考慮
    8. 避免類型轉換
      隱式轉換導致索引失效,這一點應當引起重視,也是開發中經常會犯的錯誤,由於表的字段t1定義爲varchar(20),但在查詢時把該字段作爲number類型以where條件傳給MySQL ,這樣會導致索引失效。

      /*低效:*/
      SELECT * FROM test WHERE t1 =13333333333;
      
      /*高效:*/
      SELECT * FROM test WHERE t1 ='13333333333';
    9. 避免索引列參雜計算
      檢索條件中索引列被參與計算,或被用作函數的參數,那麼就會失去該列的索引功能,從而導致性能急劇下降。可以通過建函數索引的方法,計算結果或函數值事前計算好作爲索引來用。

      /*低效:*/
      SELECT * FROM test WHERE sal*1.1 > 950;
      /*高效:*/
      SELECT * FROM test WHERE sal > 950/1.1;
      
      /*低效:*/
      SELECT * FROM test WHERE name || type = 'XXXY' ;
      /*高效:*/ 
      SELECT * FROM test WHERE name = 'XXX' AND type = 'Y' ;
      
      /*低效:*/
      SELECT * FROM test WHERE TO_CHAR(hiredate, ‘YYYYMMDD’) = '20100722';
      /*高效:*/
      SELECT * FROM test WHERE hiredate = TO_DATE('20100722' , 'YYYYMMDD') ;
      
      /*低效:*/
      SELECT * FROM test WHERE SUBSTR(name, 1, 7) = 'CAPTIAL';
      /*高效:*/
      SELECT * FROM test WHERE name LIKE 'CAPTIAL%';
      
      /*低效:*/
      SELECT * FROM test WHERE TRUNC(trans_date) = TRUNC(SYSDATE);
      /*高效:*/
      SELECT * FROM test WHERE trans_date BETWEEN TRUNC(SYSDATE) AND TURNC(SYSDATE) + .99999

      上面介始一些數據庫設計和寫sql時需要關注的地方,但是數據庫設計並不是死板的,它是靈活的,在設計數據庫表結構時,需要更多的去考慮是否符合現有業務需求,以及以後的一個擴展,這些都需要跟產品設計人員和dba進行反覆的討論,才能得出結論。

redis 的使用場景

  1. 緩存
    現在大部分程序員都知道redis可以用來作分佈式緩存,但是瞭解redis麼?redis有哪些數據類型?redis在做分佈式緩存方面有哪些需要注意的呢?如果不知道的可以往下看,知道的,但是還是想了解一下,也可以往下看。

    • redis的數據類型

      1. string
        string類型非常明顯,就是存放字符串的,當然也可以存放二進制內容。可以存數值,然後使用INCR,INCRBY,INCRBYFLOAT命令對數值進行增量操作, 如果把第二參數設置爲負數,也可以進行減量操作。部分命令如下:

        命令 命令格式 描述
        APPEND APPEND key value 如果 key 已經存在並且是一個字符串, APPEND 命令將 value 追加到 key 原來的值的末尾。如果 key 不存在, APPEND 就簡單地將給定 key 設爲 value ,就像執行 SET key value 一樣。
        GETSET GETSET key value 將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。
        DECR DECR key 將 key 中儲存的數字值減一。如果 key 不存在,那麼 key 的值會先被初始化爲 0 ,然後再執行 DECR 操作。如果值包含錯誤的類型,或字符串類型的值不能表示爲數字,那麼返回一個錯誤。本操作的值限制在 64 位(bit)有符號數字表示之內。
        DECRBY DECRBY key decrement 將 key 所儲存的值減去減量 decrement 。如果 key 不存在,那麼 key 的值會先被初始化爲 0 ,然後再執行 DECRBY 操作。如果值包含錯誤的類型,或字符串類型的值不能表示爲數字,那麼返回一個錯誤。本操作的值限制在 64 位(bit)有符號數字表示之內。
        INCR INCR key 將 key 中儲存的數字值增一。如果 key 不存在,那麼 key 的值會先被初始化爲 0 ,然後再執行 INCR 操作。如果值包含錯誤的類型,或字符串類型的值不能表示爲數字,那麼返回一個錯誤。本操作的值限制在 64 位(bit)有符號數字表示之內。
        INCRBY INCRBY key increment 將 key 所儲存的值加上增量 increment 。如果 key 不存在,那麼 key 的值會先被初始化爲 0 ,然後再執行 INCRBY 命令。如果值包含錯誤的類型,或字符串類型的值不能表示爲數字,那麼返回一個錯誤。本操作的值限制在 64 位(bit)有符號數字表示之內。
        INCRBYFLOAT INCRBYFLOAT key increment 爲 key 中所儲存的值加上浮點數增量 increment 。如果 key 不存在,那麼 INCRBYFLOAT 會先將 key 的值設爲 0 ,再執行加法操作。如果命令執行成功,那麼 key 的值會被更新爲(執行加法之後的)新值,並且新值會以字符串的形式返回給調用者。更多介紹請看http://doc.redisfans.com/string/incrbyfloat.html
        STRLEN STRLEN key 返回 key 所儲存的字符串值的長度。
        SETEX SETEX key seconds value 將值 value 關聯到 key ,並將 key 的生存時間設爲 seconds (以秒爲單位)。如果 key 已經存在, SETEX 命令將覆寫舊值。
        SETNX SETNX key value 將 key 的值設爲 value ,當且僅當 key 不存在。若給定的 key 已經存在,則 SETNX 不做任何動作。SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫

        更多的命令可以查看http://doc.redisfans.com/string/index.html

      2. list
        這個是列表類型,它提供了列表的功能,但是它也可以充當隊列的解決,它提供了以下幾個隊列的功能:

        命令 描述
        LPOP 移除並返回列表 key 的頭元素。
        RPOP 移除並返回列表 key 的尾元素。
        BLPOP 它是 LPOP 命令的阻塞版本,當給定列表內沒有任何元素可供彈出的時候,連接將被 BLPOP 命令阻塞,直到等待超時或發現可彈出元素爲止。接受超時時間
        BRPOP 它是 RPOP 命令的阻塞版本,當給定列表內沒有任何元素可供彈出的時候,連接將被 BRPOP 命令阻塞,直到等待超時或發現可彈出元素爲止。接受超時時間
        RPOPLPUSH 該命令在一個原子時間內,執行以下兩個動作:(1)將列表 source 中的最後一個元素(尾元素)彈出,並返回給客戶端。(2)將 source 彈出的元素插入到列表 destination ,作爲 destination 列表的的頭元素。
        BRPOPLPUSH BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本,當給定列表 source 不爲空時, BRPOPLPUSH 的表現和 RPOPLPUSH 一樣。當列表 source 爲空時, BRPOPLPUSH 命令將阻塞連接,直到等待超時,或有另一個客戶端對 source 執行 LPUSH 或 RPUSH 命令爲止。接受超時時間

        更多的命令可以查看http://doc.redisfans.com/list/index.html

      3. set
        這是集合類型,重複的元素將不會被添加進去,看下面的例子

        
        # 添加單個元素
        
        
        redis> SADD bbs "discuz.net"
        (integer) 1
        
        
        
        # 添加重複元素
        
        
        redis> SADD bbs "discuz.net"
        (integer) 0
        
        
        
        # 添加多個元素
        
        
        redis> SADD bbs "tianya.cn" "groups.google.com"
        (integer) 2
        
        redis> SMEMBERS bbs
        1) "discuz.net"
        2) "groups.google.com"
        3) "tianya.cn"

        更多的命令可以查看http://doc.redisfans.com/set/index.html

      4. sortset
        這個類型跟set類似,但是它是可排序的,除了增刪除改查外,比較常用的命令還有:

        命令 命令格式 描述
        ZINCRBY ZINCRBY key increment member 爲有序集 key 的成員 member 的 score 值加上增量 increment 。可以通過傳遞一個負數值 increment ,讓 score 減去相應的值,比如 ZINCRBY key -5 member ,就是讓 member 的 score 值減去 5 。當 key 不存在,或 member 不是 key 的成員時, ZINCRBY key increment member 等同於 ZADD key increment member 。
        ZCOUNT ZCOUNT key min max 返回有序集 key 中, score 值在 min 和 max 之間(默認包括 score 值等於 min 或 max )的成員的數量。
        ZRANGE ZRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定區間內的成員。其中成員的位置按 score 值遞增(從小到大)來排序。具有相同 score 值的成員按字典序(lexicographical order )來排列。如果你需要成員按 score 值遞減(從大到小)來排列,請使用 ZREVRANGE 命令。
        ZRANGEBYSCORE ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介於 min 和 max 之間(包括等於 min 或 max )的成員。有序集成員按 score 值遞增(從小到大)次序排列。具有相同 score 值的成員按字典序(lexicographical order)來排列(該屬性是有序集提供的,不需要額外的計算)。
        ZRANK ZRANK key member 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞增(從小到大)順序排列。排名以 0 爲底,也就是說, score 值最小的成員排名爲 0 。使用 ZREVRANK 命令可以獲得成員按 score 值遞減(從大到小)排列的排名。
        ZREVRANK ZREVRANK key member 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞減(從大到小)排序。排名以 0 爲底,也就是說, score 值最大的成員排名爲 0 。

      還有更多的命令可以到查看http://doc.redisfans.com/sorted_set/index.html

      1. hash
        這是個哈希表,類似於Java中的HashMap,常用的命令如下:

        命令 命令格式 描述
        HSET HSET key field value 將哈希表 key 中的域 field 的值設爲 value 。如果 key 不存在,一個新的哈希表被創建並進行 HSET 操作。如果域 field 已經存在於哈希表中,舊值將被覆蓋。
        HSETNX HSETNX key field value 將哈希表 key 中的域 field 的值設置爲 value ,當且僅當域 field 不存在。若域 field 已經存在,該操作無效。如果 key 不存在,一個新哈希表被創建並執行 HSETNX 命令。
        HMSET HMSET key field value [field value …] 同時將多個 field-value (域-值)對設置到哈希表 key 中。此命令會覆蓋哈希表中已存在的域。如果 key 不存在,一個空哈希表被創建並執行 HMSET 操作。
        HMGET HMGET key field [field …] 返回哈希表 key 中,一個或多個給定域的值。如果給定的域不存在於哈希表,那麼返回一個 nil 值。因爲不存在的 key 被當作一個空哈希表來處理,所以對一個不存在的 key 進行 HMGET 操作將返回一個只帶有 nil 值的表。
        HLEN HLEN key 返回哈希表 key 中域的數量。
        HKEYS HKEYS key 返回哈希表 key 中的所有域。
        HVALS HVALS key 返回哈希表 key 中所有域的值。
        HGET HGET key field 返回哈希表 key 中給定域 field 的值。
        HGETALL HGETALL key 返回哈希表 key 中,所有的域和值。在返回值裏,緊跟每個域名(field name)之後是域的值(value),所以返回值的長度是哈希表大小的兩倍。
        HEXISTS HEXISTS key field 查看哈希表 key 中,給定域 field 是否存在。
        HDEL HDEL key field [field …] 刪除哈希表 key 中的一個或多個指定域,不存在的域將被忽略。
        HINCRBY HINCRBY key field increment 爲哈希表 key 中的域 field 的值加上增量 increment 。增量也可以爲負數,相當於對給定域進行減法操作。如果 key 不存在,一個新的哈希表被創建並執行 HINCRBY 命令。如果域 field 不存在,那麼在執行命令前,域的值被初始化爲 0 。對一個儲存字符串值的域 field 執行 HINCRBY 命令將造成一個錯誤。本操作的值被限制在 64 位(bit)有符號數字表示之內。
        HINCRBYFLOAT HINCRBYFLOAT key field increment 爲哈希表 key 中的域 field 加上浮點數增量 increment 。如果哈希表中沒有域 field ,那麼 HINCRBYFLOAT 會先將域 field 的值設爲 0 ,然後再執行加法操作。如果鍵 key 不存在,那麼 HINCRBYFLOAT 會先創建一個哈希表,再創建域 field ,最後再執行加法操作。當以下任意一個條件發生時,返回一個錯誤:(1)域 field 的值不是字符串類型(因爲 redis 中的數字和浮點數都以字符串的形式保存,所以它們都屬於字符串類型)。(2)域 field 當前的值或給定的增量 increment 不能解釋(parse)爲雙精度浮點數(double precision floating point number)。
  2. Redis 不支持回滾(roll back)
    如果你有使用關係式數據庫的經驗, 那麼 “Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令”這種做法可能會讓你覺得有點奇怪。
    以下是這種做法的優點:

    • Redis 命令只會因爲錯誤的語法而失敗(並且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
    • 因爲不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。

    有種觀點認爲 Redis 處理事務的做法會產生 bug , 然而需要注意的是, 在通常情況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 如果你本來想通過 INCR 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR , 回滾是沒有辦法處理這些情況的。
    鑑於沒有任何機制能避免程序員自己造成的錯誤, 並且這類錯誤通常不會在生產環境中出現, 所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。

    使用事務時可能會遇上以下兩種錯誤:

    1. 事務在執行 EXEC 之前,入隊的命令可能會出錯。比如說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其他更嚴重的錯誤,比如內存不足(如果服務器使用 maxmemory 設置了最大內存限制的話)。
    2. 命令可能在 EXEC 調用之後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。
      對於發生在 EXEC 執行之前的錯誤,客戶端以前的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那麼入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那麼大部分客戶端都會停止並取消這個事務。

    不過,從 Redis 2.6.5 開始,服務器會對命令入隊失敗的情況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。
    在 Redis 2.6.5 以前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。 而新的處理方式則使得在流水線(pipeline)中包含事務變得簡單,因爲發送事務和讀取事務的回覆都只需要和服務器進行一次通訊。
    至於那些在 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行。
    從協議的角度來看這個問題,會更容易理解一些。 以下例子中, LPOP 命令的執行將出錯, 儘管調用它的語法是正確的:

    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    
    MULTI
    +OK
    
    SET a 3
    abc
    
    +QUEUED
    LPOP a
    
    +QUEUED
    EXEC
    
    *2
    +OK
    -ERR Operation against a key holding the wrong kind of value

    EXEC 返回兩條批量回復(bulk reply): 第一條是 OK ,而第二條是 -ERR 。 至於怎樣用合適的方法來表示事務中的錯誤, 則是由客戶端自己決定的。
    最重要的是記住這樣一條, 即使事務中有某條/某些命令執行失敗了, 事務隊列中的其他命令仍然會繼續執行 —— Redis 不會停止執行事務中的命令。

  3. redis做爲分佈式緩存的注意事項
    作爲緩存服務器,如果不加以限制內存的話,就很有可能出現將整臺服務器內存都耗光的情況,可以在redis的配置文件裏面設置:

    
    # 限定最多使用1.5GB內存
    
    maxmemory 1536mb

    如果內存到達了指定的上限,還要往redis裏面添加更多的緩存內容,需要設置清理內容的策略:

    
    # 設置策略爲清理最少使用的key對應的數據
    
    maxmemory-policy allkeys-lru

    清理策略有多種,redis的官方文檔有一篇很詳細的說明: http://redis.io/topics/lru-cache
    redis提供了INFO這個命令,能夠隨時監控服務器的狀態,只用telnet到對應服務器的端口,執行命令即可:

    telnet localhost 6379
    info

    在輸出的信息裏面有這幾項和緩存的狀態比較有關係:

    keyspace_hits:14414110
    keyspace_misses:3228654
    used_memory:433264648
    expired_keys:1333536
    evicted_keys:1547380

    通過計算hits和miss,我們可以得到緩存的命中率:14414110 / (14414110 + 3228654) = 81% ,一個緩存失效機制,和過期時間設計良好的系統,命中率可以做到95%以上,對於整體性能提升是很大的。
    used_memory,expired_keys,evicted_keys這3個信息的具體含義,redis的官方也有一篇很詳細的說明: http://redis.io/commands/info
    有個ruby gem叫redis-stat,它利用INFO命令展現出更直觀的信息報表,推薦:
    https://github.com/junegunn/redis-stat

    redis 緩存的單個value不能太大,因爲redis的工作線程是單線程模式,redis採用自己實現的事件分離器,效率比較高,內部採用非阻塞的執行方式,吞吐能力比較大。
    不過,因爲一般的內存操作都是簡單存取操作,線程佔用時間相對較短,主要問題在io上,因此,redis這種模型是合適的,但是如果某一個線程出現問題導致線程佔用很長時間,那麼reids的單線程模型效率可想而知.
    不過,因爲一般的內存操作都是簡單存取操作,線程佔用時間相對較短,主要問題在io上,因此,redis這種模型是合適的,但是如果某一個線程出現問題導致線程佔用很長時間,那麼reids的單線程模型效率可想而知.

    引自網絡:
    總體來說快速的原因如下: 
1)絕大部分請求是純粹的內存操作(非常快速) 
2)採用單線程,避免了不必要的上下文切換和競爭條件 
3)非阻塞IO 
內部實現採用epoll,採用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連接都轉化成了事件,然後利用epoll的多路複用特性,絕不在io上浪費一點時間 

這3個條件不是相互獨立的,特別是第一條,如果請求都是耗時的,採用單線程吞吐量及性能可想而知了。應該說redis爲特殊的場景選擇了合適的技術方案。

    而且一個字符串類型的value最式可以容納的數據長度是512M,但是如果存一個1M的value進去,當讀取到這個value時,在1000M光纖的網絡環境中,不算其它消耗,光算網絡消耗,redis的吞吐量就下降了1/1000,這在高併發的情況下,異常可怕,如果有1000個請求都讀到這個value,那麼redis的吞吐量就只有1000了。

    1. 所以redis中存儲的每一個value的字節大小不能大於5kb,如果大於5kb,就另外存一個key.
    2. 在存入每個緩存時,都要考慮一下這個緩存的時效,給出相應的過期時間.(請查看EXPIREAT和EXPIRE命令)

    參考資料:
    http://www.open-open.com/lib/view/open1419670554109.html
    https://my.oschina.net/zhenglingfei/blog/409925
    http://www.tuicool.com/articles/UjyEZn

  4. redis緩存例子-session共享
    新建一個SessionManager類,繼承org.apache.catalina.session.ManagerBase類,實現org.apache.catalina.Lifecycle接口,在其中重寫以下方法:

    //org.apache.catalina.session.ManagerBase
    Session createSession(String sessionId) 
    Session findSession(String id)
    Session createEmptySession() 
    void remove(Session session) 
    
    //org.apache.catalina.Lifecycle
    addLifecycleListener(LifecycleListener listener) 
    LifecycleListener[] findLifecycleListeners() 
    void removeLifecycleListener(LifecycleListener listener) 

    在tomcat的context.xml中加入

    <Manager className="**.****.**SessionManager"/>

    這樣就好了。
    可以參考redisson-tomcat,目前支持tomcat6,tomcat7,tomcat8 。 github項目路徑在https://github.com/redisson/redisson/tree/master/redisson-tomcat

  5. 全局sequence生成器
    使用string類型的INCR命令就可以實現了。

    redis> SET page_view 20
    OK
    
    redis> INCR page_view
    (integer) 21
    
    redis> GET page_view    # 數字值在 Redis 中以字符串的形式保存
    "21"
  6. 隊列
    使用list(列表)類型的LPOP,RPOP,RPOPLPUSH,BLPOP,BRPOP,BRPOPLPUSH就可以實現。

    redis> LLEN course
    (integer) 0
    
    redis> RPUSH course algorithm001
    (integer) 1
    
    redis> RPUSH course c++101
    (integer) 2
    
    redis> LPOP course  # 移除頭元素
    "algorithm001"
  7. 監聽器(發佈訂閱)
    有興趣的同學可以查看http://doc.redisfans.com/topic/pubsub.html

  8. 分佈式鎖
    redisson是一個用於連接redis的java客戶端工作,相對於jedis,是一個採用異步模型,大量使用netty promise編程的客戶端框架。
    redisson通過redis實現了不少功能, 分佈式鎖就是其中一種,在其中有一個RLock接口,而其實現類是RedissonLock, 下面來大概看看實現原理. 先看看 (3) 中例子執行時, 所運行的命令(通過monitor命令):

    127.0.0.1:6379> monitor
    OK
        1434959509.494805 [0 127.0.0.1:57911] "SETNX" "haogrgr" "{\"@class\":\"org.redisson.RedissonLock$LockValue\",\"counter\":1,\"id\":\"c374addc-523f-4943-b6e0-c26f7ab061e3\",\"threadId\":1}"
        1434959509.494805 [0 127.0.0.1:57911] "GET" "haogrgr"
        1434959509.524805 [0 127.0.0.1:57911] "MULTI"
        1434959509.529805 [0 127.0.0.1:57911] "DEL" "haogrgr"
        1434959509.529805 [0 127.0.0.1:57911] "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"
        1434959509.529805 [0 127.0.0.1:57911] "EXEC"
    可以看到, 大概原理是, 通過判斷Redis中是否有某一key, 來判斷是加鎖還是等待, 最後的publish是一個解鎖後, 通知阻塞在lock的線程.
    分佈式鎖的實現依賴的單點, 這裏Redis就是單點, 通過在Redis中維護狀態信息來實現全局的鎖. RedissonLock還實現了可重入, 保證原子性等等細節.
    

    想了解Redisson的同學可以查看它的git項目https://github.com/redisson/redisson, https://github.com/hkleecn/redisson 這兩網站我目前也不知道哪個是正版。
    另外參考了:
    http://www.tuicool.com/articles/BjyeaeQ
    http://blog.csdn.net/lzlhen1988/article/details/47832237
    http://yunshen0909.iteye.com/blog/2291217

功能設計需要避免的一些經驗分享

  1. C 端應用在讀到列數據時,需分頁,不能讓單個請求佔用數據庫連接,web 容器線程的時間太久,不然高併發下的吞吐量就會受到影響。
  2. 對於一些付費的第三方接口,需要按業務做嚴格的校驗。

    1. 比如發送驗證碼短信

      • 一個手機號要有指定的一個較短時間內(一分鐘)不能再次發送
      • 一個小機號指定時間段內(1小時)不能發送指定條數。
      • 一個ip一段時間內發送的短信不能超過指定數量

      類似於這樣的校驗需要有。

    2. 身份證驗證也是一樣要有時間上的校驗,不然一旦被人攻擊,將會付出大額的金錢。

上面這兩個例子是因爲我們會接觸到,所以當典型例子來列舉,當然還會有其它的場景,大家在涉及這種類型的業務時,需要注意多思考一下安全、防刷方面的校驗。

交流題

最後給兩個交流題吧,看看哪些猿的腦闊裏面裝的貸比較多。
1. 一個百萬級別的數據庫表,如果要儘快的從數據庫中導出到一個文件中,你會怎麼樣去設計?
2. 一段五十萬個字的字符串,要怎麼設計才能儘量快的從中隨機查找一個段相連的字符串?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章