1. 程式人生 > >美團實習記錄

美團實習記錄

記錄在美團實習遇到的問題以及自己的思考和解決方案等。

MAC使用起來是真的舒服啊=。= monaco 字型看起來是真的舒服啊。

封裝,封裝,封裝。 解耦,解耦,解耦。

這是樓主在美團實習最大的感觸。 你可以從技術層面(面向物件)解析,也可以從公司組織架構來看。 人們常說的面向物件的三大基本特質:封裝、繼承、多型。 其實本質上就是為了解耦。

從此多了一個外號:CRUD_輝。 =。= 在一個龐大的機器上作為一個小螺絲,擰啊擰啊擰啊擰,希望早一點發現自己的價值。

這裡寫圖片描述

谷妹復活

nslookup -q=TXT _netblocks.google.com 8.8.8.8
Last login: Wed Jun  6 11:21:19 on ttys001

# n3verl4nd @ N3verL4nd in ~ [11:24:24]
$ nslookup
> server 8.8.8.8
Default server: 8.8.8.8
Address: 8.8.8.8#53
> set type=txt
> google.com
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
google.com	text = "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"
google.com	text = "v=spf1 include:_spf.google.com ~all"
google.com	text = "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"

Authoritative answers can be found from:
> _spf.google.com
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
_spf.google.com	text = "v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all"

Authoritative answers can be found from:
> _netblocks.google.com
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
_netblocks.google.com	text = "v=spf1 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ip4:66.249.80.0/20 ip4:72.14.192.0/18 ip4:74.125.0.0/16 ip4:108.177.8.0/21 ip4:173.194.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 ip4:216.239.32.0/19 ~all"

Authoritative answers can be found from:
> _netblocks2.google.com
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
_netblocks2.google.com	text = "v=spf1 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36 ~all"

Authoritative answers can be found from:
> _netblocks3.google.com
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
_netblocks3.google.com	text = "v=spf1 ip4:172.217.0.0/19 ip4:172.217.32.0/20 ip4:172.217.128.0/19 ip4:172.217.160.0/20 ip4:172.217.192.0/19 ip4:108.177.96.0/19 ~all"

Authoritative answers can be found from:
>

或者簡單點:

dig TXT +short _netblocks{,2,3}.google.com | tr ' ' '\n' | grep '^ip4:' | sed 's/ip4://'
$ dig TXT +short _netblocks{,2,3}.google.com | tr ' ' '\n' | grep '^ip4:' | sed 's/ip4://'
64.233.160.0/19
66.102.0.0/20
66.249.80.0/20
72.14.192.0/18
74.125.0.0/16
108.177.8.0/21
173.194.0.0/16
209.85.128.0/17
216.58.192.0/19
216.239.32.0/19
172.217.0.0/19
172.217.32.0/20
172.217.128.0/19
172.217.160.0/20
172.217.192.0/19
108.177.96.0/19

在 SpringMVC 中如何持有全域性共享變數

藉助於執行緒共享變數–ThreadLocal。所謂的執行緒共享僅僅針對該執行緒,執行緒外是不可見的。

HTTP:基於請求和響應的無狀態的通訊協議。沒有請求就沒有響應。 SpringMVC 基於 Servlet 實現,預設是以單例模式執行的。所以就會有執行緒安全問題。 例如在 Controller 中建立普通的例項變數就會有問題。 一次請求和響應的處理對應執行緒池裡的一個執行緒。

而使用者 token 等資料就可以放在ThreadLocal中,起到全域性共享的目的。

public class UserTokenThreadLocal {
    private static ThreadLocal<String> contents = new ThreadLocal<>();

    public static void set(String token) {
        contents.set(token);
    }

    public static String get() {
        return contents.get();
    }

    public static void clear() {
        contents.remove();
    }
}

org.springframework.web.context.request.RequestContextHolder 同樣也是使用了 ThreadLocal。

可以在 HandlerInterceptor 的 preHandle 方法存入 token 資料,afterCompletion 方法裡面清除資料。

解決跨域

import com.sankuai.security.sdk.SecSdk;
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.*;
import java.io.IOException;

public class WebContextFilter implements Filter {    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        String[] allowedDomain = {"*.aaa.com", "*.bbb.com"};
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String origin = request.getHeader("Origin");
        if (SecSdk.SecurityCORS(origin, allowedDomain)) {
            response.addHeader("Access-Control-Allow-Origin", origin);
            response.addHeader("Access-Control-Allow-Credentials", "true");
        }
        response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, access_token, token, Content-Type, Accept, __skcy");
        response.addHeader("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, DELETE, OPTIONS");

        chain.doFilter(req, res);
    }
    
    @Override
    public void destroy() {

    }
}

也就是在響應頭中加入Access-Control-Allow-Origin與欄位。 如果想支援所有的跨域訪問,則把Origin置為請求的協議://域名:埠

快取擊穿

這裡寫圖片描述

快取擊穿 查詢一個數據庫中不存在的資料,比如商品詳情,查詢一個不存在的 ID,每次都會訪問DB,如果有人惡意破壞,很可能直接對 DB 造成過大的壓力。

快取擊穿的解決方案 當通過某一個 key 去查詢資料的時候,如果對應的記錄在資料庫中都不存在,我們將此 key對應的 value 設定為一個預設的值,比如“NULL”,並設定一個快取的失效時間,這時在快取失效之前,所有通過此key的訪問都被快取擋住了。後面如果此 key 對應的資料在 DB 中存在時,快取失效之後,通過此key再去訪問資料,就能拿到新的 value 了。

@CacheCutIn(prefix = CacheConstants.WORLDCUP_INVITECODE_INVITE_USERS, type = CacheCutInType.EFFECTIVE, expire = 3600)
@Override                                                                                                            
public EntityCache<List<WorldCupUser>> getAllInviteUsers(@CacheKey("userId") long userId) {                          
    List<WorldCupInviteUser> worldCupInviteUsers = worldCupInviteUserMapper.selectAllInviteUsers(userId);            
    if (worldCupInviteUsers == null) {
    // 防止空刷資料庫                                                                               
        return new EntityCache<>();                                                                                  
    }                                                                                                                
    List<WorldCupUser> result = new ArrayList<>();                                                                   
    for (WorldCupInviteUser worldCupInviteUser : worldCupInviteUsers) {                                              
        result.add(worldCupUserMapper.selectUserByUserId(worldCupInviteUser.getUserId()));                           
    }                                                                                                                
    return new EntityCache<>(result);                                                                                
}                                                                                                                    

token 驗證

團團內部都是使用token來驗證使用者的登陸狀態。 處理流程:

  • 客戶端使用使用者名稱跟密碼請求登入
  • 服務端收到請求,去驗證使用者名稱與密碼
  • 驗證成功後,服務端會簽發一個 Token,再把這個 Token 傳送給客戶端
  • 客戶端收到 Token 以後可以把它儲存起來,比如放在 Cookie 裡或者 Local Storage 裡
  • 客戶端每次向服務端請求資源的時候需要帶著服務端簽發的 Token
  • 服務端收到請求,然後去驗證客戶端請求裡面帶著的 Token,如果驗證成功,就向客戶端返回請求的資料

團團的處理流程是: 登陸 app 產生 token,用於整個系統的登陸驗證,驗證部分採用thrift RPC框架。本質上就是單點登入。

註解

註解很大程度上來說就是標記介面(marker interface)的替代品。

// 只用於方法上
@Target({ElementType.METHOD})
// 註解保留到執行時
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLogin {
    boolean value() default false;
}

作用:標示類的功能。

邀請碼生成器

剛來實習就有一個專案,我只負責邀請碼這一塊。 邀請碼長度被限定在5位。 最優的解決方法是根據使用者ID獲得邀請碼,但是美團的使用者id貌似不是自增的。具有隨機性,只能作罷。 那就只能隨機生成,然後藉助資料庫實現唯一性。考慮到參加本次活動的人數不會太多,該解決方案還是可以的。

解決方法: 邀請碼欄位設定唯一性索引。 直接插入邀請碼,捕獲DuplicateKeyException異常。出現異常只能去資料庫查詢唯一的邀請碼。

匯出資料:

echo "SMEMBERS inviteCode" | redis-cli -h 127.0.0.1 -a '密碼'  > ~/test_keys.txt
more ~/test_keys.txt
PNJMV
UVNTD
NPHJP
9Y2JK
KT9DG
RG89Q
CDG3A
SDRGH
NGVS4
DBZDW
HFSXE
T86CN
$ awk '{print "sadd inviteCode "$0}' ~/test_keys.txt  >~/redis.txt
more ~/redis.txt
sadd inviteCode PNJMV
sadd inviteCode UVNTD
sadd inviteCode NPHJP
sadd inviteCode 9Y2JK
sadd inviteCode KT9DG
sadd inviteCode RG89Q
sadd inviteCode CDG3A
sadd inviteCode SDRGH
sadd inviteCode NGVS4
sadd inviteCode DBZDW
127.0.0.1:6379> scard inviteCode
(integer) 28629151
127.0.0.1:6379> flushdb
OK
(21.23s)
127.0.0.1:6379>

批量匯入

cat redis.txt | redis-cli --pipe

ERR unknown command ‘add’ 執行如下轉換:

use unix2dos redis-mass-insert-office-locations.txt to convert the line breaks to \r\n

 time cat ~/redis.txt | redis-cli -a lgh123 --pipe

All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 28629151
cat ~/redis.txt  0.05s user 0.31s system 0% cpu 1:42.58 total
redis-cli -a lgh123 --pipe  3.51s user 1.03s system 4% cpu 1:42.71 total

需求又改了,邀請碼不區分大小寫了。是的啊,讓我輸入5個不區分大小寫的字母,我也是暈的一逼啊。那麼就只有 31^5(28629151) 個邀請碼了。

一次性生成 28629151 個邀請碼耗時94秒,佔用redis記憶體

used_memory:1643668000
used_memory_human:1.53G
package com.meituan.fe.evolve.activity.util;

import scala.collection.mutable.StringBuilder;

import java.util.Random;

/**
* @file InviteCodeUtils.java
* @brief 生成邀請碼的工具類
* @author liguanghui02
* @date 2018/5/21
*/
public class InviteCodeUtils {
    /** 隨機因子(刪除了 iloILO01 容易混淆的字元) */
    public static final String CODE = "abcdefghjkmnpqrstuvwxyz23456789ABCDEFGHJKMNPQRSTUVWXYZ";

    /** 隨機因子的長度 */
    public static final int CODE_LENGTH = CODE.length();
    /** 邀請碼的長度 */
    public static final int INVITE_CODE_LENGTH = 5;

    /** 隨機數生成器 */
    private static final Random RANDOM = new Random();

    /**
     * 邀請碼生成演算法
     * 理論上能夠生成 54^5(459165024) 個邀請碼
     * 考慮到參與活動的使用者資料量不大,所以產生碰撞的機率不大
     */
    public static String getInviteCode() {
        StringBuilder sb = new StringBuilder(10);
        for (int i = 0; i < INVITE_CODE_LENGTH; i++) {
            sb.append(CODE.charAt(RANDOM.nextInt(CODE_LENGTH)));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(getInviteCode());
    }

}

一般的,資料庫預設大小寫不敏感。 需要我們配置下。

alter table users modify inviteCode varchar(5) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '邀請碼';

內嵌jetty伺服器

package cn.bjut.boot;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class Bootstrap {
    public static void main(String[] args) {
        Server server = new Server(8080);
        server.setStopAtShutdown(true);
        WebAppContext context = new WebAppContext();
        context.setContextPath("/SpringMVC");
        context.setDescriptor("src/main/webapp/WEB-INF/web.xml");
        context.setResourceBase("src/main/webapp");
        server.setHandler(context);
        try {
            server.start();
            server.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

免去servlet容器的配置。

如何避免使用者併發請求

自己分管的邀請碼一塊,慘遭黃牛的併發註冊。原來限定一個使用者一天只能邀請10人,檢視資料庫有的人居然邀請了600+的人數。

解決方案: 前端:

  • 當單擊提交按鈕時,用js禁用按鈕,防止重複單擊。這點對黃牛無用啊,因為他是在模擬傳送請求。哪還有什麼點選?

後端:

  • 新增唯一性索引。這點只能控制如邀請碼不會重複。
  • 相應的操作加鎖,這種顆粒度太大,只能讓一個執行緒訪問,拋棄。
  • 使用redis的自增操作。
// 防止使用者同時構造了多個請求
if (squirrelService.incrBy(
"category", "inviteCode" + userLoginModel.getUserId(), 1, TimeConstants.SECOND) != 1) {
            return "";
}

一秒內的請求超過一次則拋棄該請求。

        boolean isLock = false;
        try {
            // 10 seconds expire
            isLock = squirrelService.setnx(CacheConstants.ACTIVITY_TRANSIENT_COUNTER, lockKey, 10);
            if (isLock) {
             // todo
        } catch (Exception e) {
            LOGGER.error("自動發起失敗。", e);
            throw e;
        } finally {
            if (isLock) {
                squirrelService.delete(CacheConstants.ACTIVITY_TRANSIENT_COUNTER, lockKey);
            }
        }

awk 的使用

_mt_datetime	userid	sourceUserid
2018-06-14 04:47:04	1784440022	230908113
2018-06-14 06:56:25	1820262564	638882361
2018-06-14 13:13:25	1818077098	675785064

將 userid 和 sourceUserid 生成 select 語句

$ awk -F'\t' '{print "select * from worldcup_users where user_id="$2,"and invite_code="$3}' 風控拒絕的邀請.txt

java -classpath

javac -cp commons-codec-1.9.jar:commons-logging-1.2.jar:httpclient-4.4.1.jar:httpcore-4.4.1.jar:libthrift-0.11.0.jar:slf4j-api-1.7.25.jar:. Server.java

如何在編譯或者執行的時候不帶這麼長的依賴 jar 包 比如,依賴的 jar 包放在 lib 資料夾下,生成的 class 檔案放在 classes 資料夾下。 這裡寫圖片描述

// 使用萬用字元
javac -classpath ".:./lib/*" *.java -d classes
//shell 拼接
javac -classpath "$(echo lib/*.jar | tr ' ' ':')" *.java -d classes
如果您的jdk還是老版本,那麼就沒法用萬用字元了,就只能一個一個寫了,或者如果是在unix系統中,可以用shell的功能把路徑下的所有jar檔案拼接起來,
比如 java -classpath $(echo libs/*.jar | tr ' ' ':') Test

那麼java6以後的萬用字元怎麼用呢?
我們看看這個例子
java -classpath "./libs/*" Test
這裡的*是指libs目錄裡的所有jar檔案,不能這麼寫 java -classpath "./libs/*.jar" Test

如果libs目錄中既有jar檔案又有class檔案,我們都想引用,那麼就需要這麼寫
java -classpath "./libs/*;./libs/" Test
注意:windows系統裡的分隔符是;  Unix系統的分隔符是:

另外需要注意的就是 libs/* 不包含libs目錄下的子目錄裡的 jar檔案,比如 libs/folder1/A.jar 
如果想包含子目錄,那就需要都明確指出,比如
java -cp "./libs/*;./libs/folder1/*" Test

maven 生成可直接執行的 jar 包

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.meituan.mtthrift.test.Client</mainClass>
                            <useUniqueVersions>false</useUniqueVersions>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                        </manifest>
                        <manifestEntries>
                            <Class-Path>.</Class-Path>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <type>jar</type>
                            <includeTypes>jar</includeTypes>                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

只需要更改 mainClass 即可。 在target目錄生成lib資料夾儲存依賴的jar包。 這裡寫圖片描述

what happen? 這裡寫圖片描述

檢視jar包依賴

// maven
mvn dependency:tree
// gradle 
gradle dependencies
// 圖形化介面
gradle build --scan

思維的侷限性

前端:面向使用者程式設計。 後端:面向極客(黃牛)程式設計。 接受前端請求的一方永遠都是危險的。前端更多的應該是去展示渲染資料,而不能更多的進行邏輯處理。

double 轉 BigDecimal 丟失精度

@Test
    public void test4() {
        System.out.println(new BigDecimal(String.valueOf(0.02)));
        System.out.println(new BigDecimal(0.02));
    }

輸出: 0.02 0.0200000000000000004163336342344337026588618755340576171875

遠端debug 導致伺服器崩潰

這裡寫圖片描述

冪等

冪等性:就是使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點選而產生了副作用。舉個最簡單的例子,那就是支付,使用者購買商品使用約支付,支付扣款成功,但是返回結果的時候網路異常,此時錢已經扣了,使用者再次點選按鈕,此時會進行第二次扣款,返回結果成功,使用者查詢餘額返發現多扣錢了,流水記錄也變成了兩條。

每次付款都生成一個唯一的訂單號(guid),只需要保證付款前檢測一下該訂單id是否已經執行過這一步驟,對未執行的請求,執行操作並快取結果,而對已經執行過的訂單號,則直接返回之前的執行結果,不做任何操作。這樣可以在最大程度上避免操作的重複執行問題,快取起來的執行結果也能用於事務的控制等。

在程式設計中.一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。例如,“getUsername()和setTrue()”函式就是一個冪等函式.

redis

這裡寫圖片描述

redis set 集合中儲存有以上四個元素ismember(29043316) 為何返回false?

    @Test
    public void test8() {
        Object intVal = 29043316;
        System.out.println(intVal.getClass().getName());// java.lang.Integer
        Object longval = 29043316L;
        System.out.println(longval.getClass().getName());// java.lang.Long
    }

所以,ismember(29043316),29043316預設包裝型別為Integer,使用 29043316L 則結果正確。

限流

常見的限流演算法有:令牌桶、漏桶、計數器。

令牌桶限流

令牌桶是一個存放固定容量令牌的桶,按照固定速率往桶裡新增令牌,填滿了就丟棄令牌,請求是否被處理要看桶中令牌是否足夠,當令牌數減為零時則拒絕新的請求。令牌桶允許一定程度突發流量,只要有令牌就可以處理,支援一次拿多個令牌。令牌桶中裝的是令牌。

這裡寫圖片描述

漏桶限流

漏桶一個固定容量的漏桶,按照固定常量速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕。漏桶可以看做是一個具有固定容量、固定流出速率的佇列,漏桶限制的是請求的流出速率。漏桶中裝的是請求。

這裡寫圖片描述

訊息佇列(接收使用者請求)+ Thread.sleep() 算不算是漏桶限流? 保證【漏桶限流】接收請求的“漏桶” 足夠大。–> 訊息佇列 流速可在美團內部的配置中心配置:MCC or Lion。

計數器限流

有時我們還會使用計數器來進行限流,主要用來限制一定時間內的總併發數,比如資料庫連線池、執行緒池、秒殺的併發數;計數器限流只要一定時間內的總請求數超過設定的閥值則進行限流,是一種簡單粗暴的總數量限流,而不是平均速率限流。

Integer 快取

這裡寫圖片描述

阿里巴巴程式碼規範推薦:包裝型別間的相等判斷應該用 equals,而不是’==’

使用 mybatis 從資料庫中讀取資料的 Integer 物件。 使用 gson 轉換得到的Integer 物件。 這兩個物件能不能比較? 能不能比較看實現

所謂的 Mybatis 僅僅是對 JDBC 的封裝。獲取資料還是需要依賴底層的ResultSet。

@Override
public Integer getNullableResult(ResultSet rs, String columnName)
    throws SQLException {   
    return rs.getInt(columnName);
}

簡單點:

public class Main {
    public Integer getInt() {
        return 300;
    }
}

對應的位元組碼:

$ javap -c Main
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public java.lang.Integer getInt();
    Code:
       0: sipush        300
       3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       6: areturn
}

Integer.valueOf

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

堆中存在 【-128-127】 256 個 Integer 快取。

對於 GSON 也是同樣的道理

    @Test
    public void test3() {
        Gson gson = new Gson();
        User userFrom = new User();
        userFrom.setId(3L);
        userFrom.setAge(127);
        userFrom.setName("test");
        String json = gson.toJson(userFrom, User.class);
        System.out.println(json);
        User userTo = gson.fromJson(json, User.class);
        System.out.println(userTo.getAge() == Integer.valueOf(127));
    }

這裡寫圖片描述

所以對於 [-128-127] 的 Integer 物件是可以使用 == 進行比較的。限制條件就是使用Integer.valueOf 轉換。 當然不推薦這麼做。

csv 分割

split -l 20000 source.csv for i in *; do mv “i&quot;&quot;i&quot; &quot;i.csv”; done

shell 讀取 mysql

mysql -h IP -P 埠 -u賬號 -p密碼 資料庫 -e "select * from qixi limit 10" | awk 'NR > 1'

將檔案從stage中移除

  1. 若該檔案不在 repository 內:git rm --cached filename
  2. 若該檔案在 repository 內:git reset head filename

git rm file 是工作區和暫存區都刪除。