美团实习记录

版权声明:作者:N3verL4nd 出处: https://blog.csdn.net/lgh1992314/article/details/80356091

记录在美团实习遇到的问题以及自己的思考和解决方案等。

MAC使用起来是真的舒服啊=。=
monaco 字体看起来是真的舒服啊。

封装,封装,封装。
解耦,解耦,解耦。

这是楼主在美团实习最大的感触。
你可以从技术层面(面向对象)解析,也可以从公司组织架构来看。
人们常说的面向对象的三大基本特质:封装、继承、多态。
其实本质上就是为了解耦。

从此多了一个外号:CRUD_辉。 =。=
在一个庞大的机器上作为一个小螺丝,拧啊拧啊拧啊拧,希望早一点发现自己的价值。

这里写图片描述


谷妹复活

https://support.google.com/a/answer/60764?hl=zh-Hans

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);
            }
        }

https://tech.meili-inc.com/prevent-duplicate-requests-4

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。

计数器限流

有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。

我滴个妈,狼哥在点评写的 Rhino 中间件。学习下。
https://www.jianshu.com/u/90ab66c248e6

限流参考:
http://www.cnblogs.com/clds/p/5850070.html
https://www.cnblogs.com/softidea/p/6229543.html
https://juejin.im/entry/57cce5d379bc440063066d09
http://www.54tianzhisheng.cn/2017/09/23/Guava-limit/
https://www.cnblogs.com/haoxinyue/p/6792309.html
http://www.54tianzhisheng.cn/2017/09/23/Guava-limit/

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 是工作区和暂存区都删除。

猜你喜欢

转载自blog.csdn.net/lgh1992314/article/details/80356091
今日推荐