Mulavar

Mulavar 查看完整档案

北京编辑浙江大学  |  CS 编辑猿辅导  |  服务端开发工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Mulavar 发布了文章 · 3月3日

详解ThreadLocal原理及内存泄漏

1. ThreadLocal作用

ThreadLocal的作用是使得每个线程都能拥有各自独立的对象副本,假设多个线程拥有同一个实例,ThreadLocal<T>类型的变量在每个线程中都有一个副本,从而为变量提供了线程间隔离的作用。

2. ThreadLocal实例


public class ThreadLocalDemo implements Runnable{
    private static ThreadLocal<People> local = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo localDemo = new ThreadLocalDemo();
        Thread thread1 = new Thread(localDemo);
        thread1.start();

          Thread.sleep(1000);
      
        Thread thread2 = new Thread(localDemo);
        thread2.start();
    }

    @Override
    public void run() {
        People people = local.get();
        if (people==null) {
            people = new People();
            people.setName("djh");
            people.setAge(new Random().nextInt());
            local.set(people);
        }
        System.out.println(people);
    }
}

public class People {
    String name;
    int age;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

在该例子中,thread1thread2拥有同一个对象localDemo,如果不使用ThreadLocal包装,则两个线程访问的people应该是同一个。而在我们使用ThreadLocal包装后,运行结果如下:

bean.People@6fb35e7d
bean.People@614b201a

两者访问的people是不一样的两个对象,而这就是ThreadLocal的作用,提供了变量线程间的隔离能力。

3. 源码解析

查看ThreadLocal的set方法,

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  1. 先获取当前线程;
  2. 获取当前线程的threadLocals变量(是一个ThreadLocalMap类型的Map);
  3. 如果map不为空,为这个map插入<当前ThreadLocal对象,值>的键值对;
  4. 如果该map为空,则创建一个一个ThreadLocalMap对象,t.threadLocals = new ThreadLocalMap(this, firstValue)

由于每个线程都有各自的threadLocals,所以多个线程运行时经过第二步拿到的map是不一样的,这样就实现了线程间隔离。

4. ThreadLocal的内存泄漏问题

由第3节可以得知,数据实际存在每个线程的threadLocals成员(即一个ThreadLocalMap对象)里,ThreadLocalMap和普通的map一样使用Entry数组存储数据,这个Entry特殊的地方在于,它继承了弱引用,即该Entry的key是一个ThreadLocal对象的弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用的特点是,如果某个对象只被弱引用关联,在下一次GC时该对象就会被回收。此时某个ThreadLocal类型变量使用完被我们置为null,此时Entry中关联的Key会变成null,但Value由于是强引用,且运行的线程指向他,因此Value部分发生了内存泄漏。

线程执行完毕被回收时Value会随之回收,但当我们使用线程池时,由于线程会复用,因此Value会一直存在,这就发生了更为严重的内存泄漏。

5. 参考资料

  1. 正确理解Thread Local的原理与适用场景
  2. ThreadLocal 内存泄漏问题深入分析
查看原文

赞 0 收藏 0 评论 0

Mulavar 关注了用户 · 2月23日

taoyq1988 @taoyq1988

关注 1

Mulavar 发布了文章 · 2月23日

Spring学习笔记-01

1. Spring简介

一句话概括Spring,Spring是一个开源的、轻量的控制反转(IOC)面向切面编程(AOP)的框架。

1.1 Spring、SpringMVC、Spring Boot和Spring Cloud的区别

SpringMVC是在Spring基础之上结合了MVC三层架构的框架,使用DispatchServlet和视图解析器做到了更方便的视图跳转和控制器调度处理,主要负责处理WEB应用开发。SpringMVC是Spring的一个子模块。

SpringBoot则使用了约定大于配置的理念,简化了Spring配置,专注于微服务的开发。

SpringCloud专注于全局微服务的整合、管理。

2. 控制反转—IOC

2.1 基本概念

IOC:控制反转,是一种设计思想。即将对象创建的控制权转移给了Spring容器

假设我们创建了类A,类A中有个成员变量类B。

  • 传统开发:我们需要手动创建类B的对象,将他赋给类A的对象,这叫主动控制对象的创建;
  • IOC:Spring创建好B对象,然后存储到一个容器里面,当A对象需要使用B对象时,Spring就从存放对象的那个容器里面取出A要使用的那个B对象。

使用传统方式,A对B产生了依赖,也就是A和B之间存在一种耦合关系(假设B的实现有了改动,我们可能还需要改动A的源码),而通过IOC的思想,这种依赖关系就被容器取代了,所有对象的创建依赖于容器,在启动的时候,Spring会读取配置文件,并将创建的对象先注入容器,之后我们想要使用直接从容器中取就可以。

DI:依赖注入,可以理解为是Spring实现IOC的具体方式。

2.2 Spring创建对象方式

class UserService {
    private UserDao dao;
    
    public void setUserDao(UserDao dao) {
        this.userDao = dao;
    }
}

class UserDao {
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
}

2.2.1 构造函数

<bean id="dao" class="com.dong.dao.UserDao">
    <!--使用name定位构造函数-->
    <constructor-arg name="name" value="dongjh"></constructor-arg>
</bean>


<bean id="service" class="com.dong.service.UserService">
    <!--使用type定位构造函数-->
    <constructor-arg type="com.dong.dao.UserDao" ref="dao"></constructor-arg>
</bean>

注:

  1. 如果某类有无参构造方法,则默认调用无参构造方法;
  2. 如果某类只有一个构造方法,则默认调用该构造方法。

2.2.2 property(调用set方法创建)

<bean id="dao" class="com.dong.service.UserDao">
    <!--基本类型使用value注入属性-->
    <property name="name" value="dongjh"></property>
</bean>

<bean id="service" class="com.dong.service.UserService">
    <!--引用类型使用ref属性-->
    <property name="userDao" ref="dao"></property>
</bean>

注:

  1. 使用属性注入时,容器会默认调用无参构造方法创建实例,所以需要保证要创建的bean有无参的构造方法;
  2. 使用属性注入等价于调用了setXXX方法,XXX是property标签中的name,假设上面的代码的setName方法改成了setName1,则第一个bean的property需这样写:
<property name="name1" value="dongjh"></property>

2.3 Bean的自动装配(autowiring)

自动装配即Spring在上下文中自动寻找,自动给bean装配属性。

bean的自动装配主要有三种策略:

  1. byname:把与某bean(A)属性具有相同名字的其他bean(B)自动装配到该bean(A)的属性中;
  2. byType:把与某bean(A)属性具有相同类型的其他bean(B)自动装配到该bean(A)的属性中;
  3. constructor:把与某bean(A)的构造器入参具有相同类型的其他bean(B)自动装配到该bean(A)的属性中;

2.3.1 byName

xml配置方式
<!--由于该bean的id为userDao,所以会直接自动装配下面service的userDao属性中-->
<bean id="userDao" class="com.dong.service.UserDao">
    <property name="name" value="dongjh"></property>
</bean>

<!--该类有setUserDao方法,且设置了属性自动装配byName-->
<bean id="service" class="com.dong.service.UserService" autowired="byName">
</bean>

2.3.2 byType

xml配置方式
<!--该bean的类型为com.dong.service.UserDao,所以会直接自动装配下面service的userDao属性中-->
<bean id="userDao111" class="com.dong.service.UserDao">
    <property name="name" value="dongjh"></property>
</bean>

<!--该类一个com.dong.service.UserDao类型的属性,且设置了属性自动装配byType-->
<bean id="service" class="com.dong.service.UserService" autowired="byType">
</bean>

2.4 Spring注解配置

JDK1.5开始支持注解,而Spring是2.5开始支持。

使用Spring注解配置需要先导入约束: context名称空间和注解扫描。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!--注:只会寻找在同一个应用上下文中定义的注解,
    比如将这个声明放置到DispatcherServlet的配置上下文中,
    它就只会检查controllers的上下文,而不会检查services的上下文,
    详情见https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-annotation-config-->
    <context:annotation-config/>
    <!--指定注解扫描返回-->
    <context:component-scan base-package="com.dong.dao"></context:component-scan>

</beans>

@Autowired

之后在需要自动装配的属性或set方法上使用@AutoWired注解,就可以实现自动装配。

//详细的可修饰范围
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})

注:@Autowired会先使用byType,再按byName的顺序自动装配。

@Autowired有一个常用属性:required,默认值为true,这个属性表示修饰的成员变量是否一定需要装配一个bean:

  • 当required设置为true时,表示必须有一个可以自动装配到该成员变量的bean,否则报错;
  • 当required设置为false时,则表示该成员变量可以为null;

@Qualifier

当有多个bean满足自动装配条件时,可以使用这个指定装配的bean,一般配合@Autowired使用。

@Qualifier有个属性value,给这个value设置想要装配的bean的id即可。

@Component

修饰类,表示将该类的bean对象注入到Spring容器中。

@Component有个属性value,表示注入时设置的id。

如:

//等价于<bean id="user" class="User">
@Component(value="user")
public class User {}

@Component有一些衍生注解,如Web开发会按照MVC架构分层,根据不同的功能层,衍生出了一些注解:

  • @Repository:针对dao层
  • @Service:针对service层
  • @Controller:针对controller层

这四个注解功能一致,都表示将某类的bean对象注入到Spring容器中。

@Value

给属性注入值,主要用于给成员变量注入值,简单的可以注入基本类型的值,复杂时可以注入一些操作系统属性或其他bean的属性。

@Scope

用于设置bean的作用域,先介绍最常见的:

  • singleton:全局唯一,单例;
  • prototype:每次注入创建一个新的对象。

注解和配置的区别

xml功能更加强大,方便管理维护。注解适用于简单的属性注入。

2.5 JavaConfig配置

Spring 3.0之后开始支持了一种更优的配置方式:基于Java类的配置方式。通过增加@Configuration注解表明这是一个配置类,其底层为@Component(即该类的bean对象也会被Spring容器接管)。

@Configuration结合@Bean注解可以实现XML配置实现注入的功能,效果与

<beans>
    <bean>...</bean>
    <bean>...</bean>
</beans>

类似,在使用@Bean注解时,表示将该方法返回的对象加载进Spring容器,在使用@Bean修饰方法时需要注意:

  1. 方法带返回值,且返回类型为想要被管理的bean的类型;
  2. 方法名即为默认的bean name,可使用@Bean的name属性自定义;
  3. 方法可带参数,则此时传入的参数也必须是被Spring容器管理的bean

以创建UserService和UserDao为例:

@Configuration
public class UserConfiguration {
    @Bean
    public UserService userService() {
        return new UserService();
    }
    
    @Bean
    public UserDao userDao() {
        return new UserDao();
    }
}

当Spring扫描该类时,会自动将UserConfiguration、UserService、UserDao的bean注入到Spring容器中进行托管。

3. 面向切面编程—AOP

3.1 基本概念

在软件开发中,我们需要编写许多的业务代码,但还有很多代码是业务无关的,如

  • 日志功能
  • 缓存清理
  • 资源回收
  • 权限检查

为了实现业务代码和非业务代码的分离,Spring利用代理模式抽出了非业务代码,形成了AOP思想。

AOP全称Aspect Oriented Programming,意为面向切面编程,假设业务代码本身是一条纵向的逻辑流,我们在其中找几个点(切点,Pointcut),插入非业务代码,就像是一个个切面(Aspect),这就是一个简单的AOP例子。

切面和切点很容易被混淆,严格来说,切点是切面的一部分信息,切面主要包含两部分信息:

  • 切入点;
  • 切入后要执行的动作(Advice)。

而在具体的实现中,Spring为切面还加了一个信息:在切点的哪个阶段执行?由此衍生出有:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法被调用之后调用通知功能;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

关于AOP在Spring的实现有三种方式:

  1. 实现原生的Spring API接口
  2. 自定义切面类
  3. 注解

3.2 实现原生Spring API接口

Spring提供了诸如MethodBeforeAdvice、AfterAdvice、AfterReturningAdvice等原生接口,我们可以通过实现这些接口构建切面,如

@Component
public class Log implements MethodBeforeAdvice {
    /**
     * @param method  要执行的目标对象方法
     * @param objects 参数
     * @param o       目标对象
     * @throws Throwable
     */
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println(o.getClass().getName() + "-" + method.getName() + "将要执行");
    }
}

该Log类实现了MethodBeforeAdvice,成为了一个在方法执行前插入的切面,其次,我们需要在XML中进行配置,设置切点并建立其和切面的连接:

<aop:config>
    <!--切入点:execution表达式,execution(要执行的方法!)-->
    <aop:pointcut id="pointcut" expression="execution(* com.dong.service.UserServiceImpl.*(..))"/>
    <aop:advisor advice-ref="log" pointcut-ref="pointcut"></aop:advisor>
</aop:config>

execution表示在方法执行时触发;*表示方法返回值可以是任意类型,com.dong.service.UserServiceImpl.*使用全限定类名和方法名指定要添加前置通知的方法,使用*表示对com.dong.service.UserServiceImpl类下的所有方法都执行切入;(..)表示方法的参数列表,使用(..)表示方法的入参可以是任意类型。

需要注意,Log必须是一个被Spring容器管理的bean对象,否则Spring将无法找到该切面的bean,因此使用@Component注解修饰(也可以使用XML配置等方式注入)。

3.3 自定义切面类

自定义切面不需要实现Spring的原生接口,首先需要定义一个切面类,并将加载给Spring容器进行托管。

@Component(value="diy")
public class DiyPointCut {
    public void before() {
        System.out.println("方法执行前");
    }

    public void after() {
        System.out.println("方法执行后");
    }
}

与3.2相似,该方式需要通过XML配置设置切点以及其和切面的连接。

<aop:config>
    <!--自定义切面,ref:要引用的bean-->
    <aop:aspect ref="diy">
        <aop:pointcut id="pointcut" expression="execution(* com.dong.service.UserServiceImpl.*(..))"/>
        <!--指定advice和切点的关系-->
        <aop:before method="before" pointcut-ref="pointcut"></aop:before>
        <aop:after method="after" pointcut-ref="pointcut"></aop:after>
    </aop:aspect>
</aop:config>

3.4 注解

3.4.1 启用注解功能

首先,使用注解有两种方式:

  1. XML导入配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            https://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!--启用注解-->
        <context:annotation-config/>、
        <!--注解包扫描-->
        <context:component-scan base-package="com.dong.dao"></context:component-scan>
        <context:component-scan base-package="com.dong.log"></context:component-scan>
        <!--启用aop注解-->
        <aop:aspectj-autoproxy proxy-target-class="true"/>
    </beans>
  2. 使用@EnableAspectJAutoProxy注解需要被切的类

    @Component(value = "userService")
    @EnableAspectJAutoProxy
    public class UserServiceImpl implements UserService {}

其次,使用注解时,需要注意两部分:

  • 声明切面:声明某类是切面以及切入后实际要执行的动作(Advice);
  • 定位切点:需要通过execution表达式定位需要切入的方法。

下面介绍一下注解的使用方式。

3.4.2 注解使用

只要对一个类使用@Aspect注解修饰,就表明这是一个切面。可以对其中的方法使用

  • @Before
  • @After
  • @AfterReturning
  • @Around
  • @AfterThrowing

注解指定切入时机,切入点通过execution表达式指定,举例如下:

@Aspect
@Component
public class CustomPointCut {
    @Before("execution(* com.dong.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("aop:方法发生前");
    }
}

4. 参考资料

  1. Spring最新5完整教程
  2. Spring入门(十):Spring AOP使用讲解
  3. Spring AOP - 注解方式使用介绍
查看原文

赞 1 收藏 1 评论 0

Mulavar 收藏了文章 · 2月23日

springboot(四)——@EnableConfigurationProperties是如何起作用的你知道吗

前言

用springboot开发的过程中,我们会用到@ConfigurationProperties注解,主要是用来把properties或者yml配置文件转化为bean来使用的,而@EnableConfigurationProperties注解的作用是@ConfigurationProperties注解生效。
如果只配置@ConfigurationProperties注解,在IOC容器中是获取不到properties配置文件转化的bean的,当然在@ConfigurationProperties加入注解的类上加@Component也可以使交于springboot管理。

举个栗子

第一步:创建一个类TestConfigurationProperties

@ConfigurationProperties(prefix = "properties")
public class TestConfigurationProperties {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

注意:得加上set和get方法
第二步:创建TestAutoConfiguration类

@Configuration
@EnableConfigurationProperties(TestConfigurationProperties.class)
public class TestAutoConfiguration {

    private TestConfigurationProperties testConfigurationProperties;

    public TestAutoConfiguration(TestConfigurationProperties testConfigurationProperties) {
        this.testConfigurationProperties = testConfigurationProperties;
    }

    @Bean
    public User user(){
        User user = new User();
        user.setName(testConfigurationProperties.getName());
        return user;
    }
}

注意:得创建一个有参构造方法
第三步:配置文件加入属性

properties.name=test

第四步:跑一下,打印出User这个类

@RestController
@RequestMapping("/api/test")
@Slf4j
public class TestController {
    @Autowired
    TestConfigurationProperties testConfigurationProperties;

    @Autowired
    User user;

    @RequestMapping(value = "/testConfigurationProperties")
    public String testConfigurationProperties() {
        log.info("test testConfigurationProperties.............{}", testConfigurationProperties.getName());
        log.info("user:{}", user);
        return "SUCCESS";
    }
}

控制台输出:

2019-04-21/16:11:36.638||||||||^_^|[http-nio-8088-exec-1] INFO  com.stone.zplxjj.controller.TestController 37 - test testConfigurationProperties.............test
2019-04-21/16:11:36.639||||||||^_^|[http-nio-8088-exec-1] INFO  com.stone.zplxjj.controller.TestController 38 - user:User(id=null, name=test)

@EnableConfigurationProperties是怎么加载的

通过查看@EnableConfigurationProperties的注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

    /**
     * Convenient way to quickly register {@link ConfigurationProperties} annotated beans
     * with Spring. Standard Spring Beans will also be scanned regardless of this value.
     * @return {@link ConfigurationProperties} annotated beans to register
     */
    Class<?>[] value() default {};

}

通过分析自动配置可以知道,肯定是这个类EnableConfigurationPropertiesImportSelector起的作用:

private static final String[] IMPORTS = {
            ConfigurationPropertiesBeanRegistrar.class.getName(),
            ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        return IMPORTS;
    }

selectImports方法返回了这两个类:ConfigurationPropertiesBeanRegistrar和ConfigurationPropertiesBindingPostProcessorRegistrar,是何时加载的,我们只需要看这个类ConfigurationPropertiesBeanRegistrar即可:

public static class ConfigurationPropertiesBeanRegistrar
            implements ImportBeanDefinitionRegistrar {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
            getTypes(metadata).forEach((type) -> register(registry,
                    (ConfigurableListableBeanFactory) registry, type));
        }
        //找到加入这个注解@EnableConfigurationProperties里面的value值,其实就是类class
        private List<Class<?>> getTypes(AnnotationMetadata metadata) {
            MultiValueMap<String, Object> attributes = metadata
                    .getAllAnnotationAttributes(
                            EnableConfigurationProperties.class.getName(), false);
            return collectClasses((attributes != null) ? attributes.get("value")
                    : Collections.emptyList());
        }

        private List<Class<?>> collectClasses(List<?> values) {
            return values.stream().flatMap((value) -> Arrays.stream((Object[]) value))
                    .map((o) -> (Class<?>) o).filter((type) -> void.class != type)
                    .collect(Collectors.toList());
        }
      //注册方法:根据找到的类名name和type,将加入注解@ConfigurationProperties的类加入spring容器里面
        private void register(BeanDefinitionRegistry registry,
                ConfigurableListableBeanFactory beanFactory, Class<?> type) {
            String name = getName(type);
            if (!containsBeanDefinition(beanFactory, name)) {
                registerBeanDefinition(registry, name, type);
            }
        }
    //找到加入注解@ConfigurationProperties的类的名称,加入一定格式的拼接
        private String getName(Class<?> type) {
            ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
                    ConfigurationProperties.class);
            String prefix = (annotation != null) ? annotation.prefix() : "";
            return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
                    : type.getName());
        }

        private boolean containsBeanDefinition(
                ConfigurableListableBeanFactory beanFactory, String name) {
            if (beanFactory.containsBeanDefinition(name)) {
                return true;
            }
            BeanFactory parent = beanFactory.getParentBeanFactory();
            if (parent instanceof ConfigurableListableBeanFactory) {
                return containsBeanDefinition((ConfigurableListableBeanFactory) parent,
                        name);
            }
            return false;
        }

        private void registerBeanDefinition(BeanDefinitionRegistry registry, String name,
                Class<?> type) {
            assertHasAnnotation(type);
            GenericBeanDefinition definition = new GenericBeanDefinition();
            definition.setBeanClass(type);
            registry.registerBeanDefinition(name, definition);
        }

        private void assertHasAnnotation(Class<?> type) {
            Assert.notNull(
                    AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
                    () -> "No " + ConfigurationProperties.class.getSimpleName()
                            + " annotation found on  '" + type.getName() + "'.");
        }

    }

结语

另外还有这个类:ConfigurationPropertiesBindingPostProcessorRegistrar,刚刚没有分析,看了下源码,其实他做的事情就是将配置文件当中的属性值赋予到加了@ConfigurationProperties的注解的类的属性上,具体就不分析了,有兴趣自己可以阅读,入口知道了,就简单了

更多文章可以关注公众号:stonezplxjj和个人博客:http://www.zplxjj.com

图片描述

查看原文

Mulavar 收藏了文章 · 2月23日

分布式事务:两阶段提交与三阶段提交

在分布式系统中著有 CAP 理论,该理论由加州大学伯克利分校的 Eric Brewer 教授提出,阐述了在一个分布式系统中不可能同时满足一致性( C onsistency)、可用性( A vailability),以及分区容错性( P artition Tolerance)。

  • 一致性: 在分布式系统中数据往往存在多个副本,一致性描述的是这些副本中的数据在内容和组织上的一致。
  • 可用性: 描述系统对用户的服务能力,所谓可用是指在用户能够容忍的时间范围内返回用户期望的结果。
  • 分区容错性: 分布式系统通常由多个节点构成,由于网络是不可靠的,所以存在分布式集群中的节点因为网络通信故障导致被孤立成一个个小集群的可能性,即网络分区,分区容错性要求在出现网络分区时系统仍然能够对外提供一致性的可用服务。

对于一个分布式系统而言,我们要始终假设网络是不可靠的,因此分区容错性是对一个分布式系统最基本的要求,我们的切入点更多的是尝试在可用性和一致性之间寻找一个平衡点,但这也并非要求我们在系统设计时一直建立在网络出现分区的前提之上,然后对一致性和可用性在选择时非此即彼。

Eric Brewer 教授在 2012 年就曾指出 CAP 理论证明不能同时满足一致性、可用性,以及分区容错性的观点在实际系统设计指导上存在一定的误导性。 传统对于 CAP 理论的理解认为在设计分布式系统时必须满足 P,然后在 C 和 A 之间进行取舍,这是片面的。实际中网络出现分区的可能性还是比较小的,尤其是目前网络环境正在变得越来越好,甚至许多系统都拥有专线支撑,所以在网络未出现分区时,还是应该兼顾 A 和 C。另外就是对于一致性、可用性,以及分区容错性三者在度量上也应该有一个评定范围,最简单的以可用性来说,当有多少占比请求出现响应超时才可以被认为是不满足可用性,而不是一出现超时就认为是不可用的。最后我们需要考虑的一点就是分布式系统一般都是一个比较大且复杂的系统,我们应该从更小的粒度上对各个子系统进行评估和设计,而不是简单的从整体上武断决策。

让分布式集群始终对外提供可用的一致性服务一直是富有挑战和趣味的任务。暂且抛开可用性,拿一致性来说,对于关系型数据库我们通常利用事务来保证数据的强一致性,但是当我们的数据量越来越大,大到单库已经无法承担时,我们不得不采取分库分表的策略对数据库实现水平拆分,或者引入 NoSQL 技术,构建分布式数据库集群以分摊读写压力,从而提升数据库的存储和响应能力,但是多个数据库实例也为我们使用数据库带来了许多的限制,比如主键的全局唯一、联表查询、数据聚合等等,另外一个相当棘手的问题就是数据库的事务由原先的单库事务变成了现在的分布式事务。

分布式事务的实现并不是无解的,比如下文要展开的两阶段提交(2PC:Two-Phase Commit)和三阶段提交(3PC:Three-Phase Commit)都给我们提供了思路,但是在分布式环境下如何保证数据的强一致性,并对外提供高可用的服务还是相当棘手的,因此很多分布式系统对于数据强一致性都敬而远之。

两阶段提交协议(2PC:Two-Phase Commit)

两阶段提交协议的目标在于为分布式系统保证数据的一致性,许多分布式系统采用该协议提供对分布式事务的支持。顾名思义,该协议将一个分布式的事务过程拆分成两个阶段: 投票事务提交 。为了让整个数据库集群能够正常的运行,该协议指定了一个 协调者 单点,用于协调整个数据库集群各节点的运行。为了简化描述,我们将数据库集群中的各个节点称为 参与者 ,三阶段提交协议中同样包含协调者和参与者这两个角色定义。

第一阶段:投票

该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:

  1. 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果;
  2. 事务参与者收到请求之后,执行事务但不提交,并记录事务日志;
  3. 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。

第二阶段:事务提交

在经过第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在 3 种可能性:

  1. 所有的参与者都回复能够正常执行事务。
  2. 一个或多个参与者回复事务执行失败。
  3. 协调者等待超时。

对于第 1 种情况,协调者将向所有的参与者发出提交事务的通知,具体步骤如下:

  1. 协调者向各个参与者发送 commit 通知,请求提交事务;
  2. 参与者收到事务提交通知之后执行 commit 操作,然后释放占有的资源;
  3. 参与者向协调者返回事务 commit 结果信息。

2pc-success

对于第 2 和第 3 种情况,协调者均认为参与者无法成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:

  1. 协调者向各个参与者发送事务 rollback 通知,请求回滚事务;
  2. 参与者收到事务回滚通知之后执行 rollback 操作,然后释放占有的资源;
  3. 参与者向协调者返回事务 rollback 结果信息。

2pc-failed

__两阶段提交协议解决的是分布式数据库数据强一致性问题__,实际应用中更多的是用来解决事务操作的原子性,下图描绘了协调者与参与者的状态转换。

2pc-state

站在协调者的角度,在发起投票之后就进入了 WAIT 状态,等待所有参与者回复各自事务执行状态,并在收到所有参与者的回复后决策下一步是发送 commit 或 rollback 信息。站在参与者的角度,当回复完协调者的投票请求之后便进入 READY 状态(能够正常执行事务),接下去就是等待协调者最终的决策通知,一旦收到通知便可依据决策执行 commit 或 rollback 操作。

两阶段提交协议原理简单、易于实现,但是缺点也是显而易见的,包含如下:

  • 单点问题

协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,就会影响整个数据库集群的正常运行。比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。

  • 同步阻塞

两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下。

  • 数据不一致性

两阶段提交协议虽然是分布式数据强一致性所设计,但仍然存在数据不一致性的可能性。比如在第二阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

针对上述问题可以引入 超时机制互询机制 在很大程度上予以解决。

对于协调者来说如果在指定时间内没有收到所有参与者的应答,则可以自动退出 WAIT 状态,并向所有参与者发送 rollback 通知。对于参与者来说如果位于 READY 状态,但是在指定时间内没有收到协调者的第二阶段通知,则不能武断地执行 rollback 操作,因为协调者可能发送的是 commit 通知,这个时候执行 rollback 就会导致数据不一致。

此时,我们可以介入互询机制,让参与者 A 去询问其他参与者 B 的执行情况。如果 B 执行了 rollback 或 commit 操作,则 A 可以大胆的与 B 执行相同的操作;如果 B 此时还没有到达 READY 状态,则可以推断出协调者发出的肯定是 rollback 通知;如果 B 同样位于 READY 状态,则 A 可以继续询问另外的参与者。只有当所有的参与者都位于 READY 状态时,此时两阶段提交协议无法处理,将陷入长时间的阻塞状态。

三阶段提交协议(3PC:Three-Phase Commit)

针对两阶段提交存在的问题,三阶段提交协议通过引入一个 预询盘 阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能。三阶段提交的三个阶段分别为:预询盘(can_commit)、预提交(pre_commit),以及事务提交(do_commit)。

第一阶段:预询盘

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

  1. 协调者向各个参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复;
  2. 各个参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息。

第二阶段:预提交

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有 3 种:

  1. 所有的参与者都返回确定信息。
  2. 一个或多个参与者返回否定信息。
  3. 协调者等待超时。

针对第 1 种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

  1. 协调者向所有的事务参与者发送事务执行通知;
  2. 参与者收到通知后执行事务但不提交;
  3. 参与者将事务执行情况返回给客户端。

在上述步骤中,如果参与者等待超时,则会中断事务。 针对第 2 和第 3 种情况,协调者认为事务无法正常执行,于是向各个参与者发出 abort 通知,请求退出预备状态,具体步骤如下:

  1. 协调者向所有事务参与者发送 abort 通知;
  2. 参与者收到通知后中断事务。

3pc-fail-1

第三阶段:事务提交

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为 3 种情况:

  1. 所有的参与者都能正常执行事务。
  2. 一个或多个参与者执行事务失败。
  3. 协调者等待超时。

针对第 1 种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

  1. 协调者向所有参与者发送事务 commit 通知;
  2. 所有参与者在收到通知之后执行 commit 操作,并释放占有的资源;
  3. 参与者向协调者反馈事务提交结果。

3pc-success

针对第 2 和第 3 种情况,协调者认为事务无法成功执行,于是向各个参与者发送事务回滚请求,具体步骤如下:

  1. 协调者向所有参与者发送事务 rollback 通知;
  2. 所有参与者在收到通知之后执行 rollback 操作,并释放占有的资源;
  3. 参与者向协调者反馈事务回滚结果。

3pc-fail-2

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的 commit 或 rollback 请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续 commit,相对于两阶段提交虽然降低了同步阻塞,但仍然无法完全避免数据的不一致。

3pc-state

两阶段提交协议中所存在的长时间阻塞状态发生的几率还是非常低的,所以虽然三阶段提交协议相对于两阶段提交协议对于数据强一致性更有保障,但是因为效率问题,两阶段提交协议在实际系统中反而更加受宠。

更多精彩内容,欢迎访问个人主页 https://www.zhenchao.org/

参考

  1. CAP 理论十二年回顾:"规则"变了
查看原文

Mulavar 赞了文章 · 2020-11-05

ThreadLocal 内存泄漏问题深入分析

写在前面

ThreadLocal 基本用法本文就不介绍了,如果有不知道的小伙伴可以先了解一下,本文只研究 ThreadLocal 内存泄漏这一问题。

ThreadLocal 会发生内存泄漏吗?

先给出结论:如果你使用不当是有可能发生内存泄露的

<br/>

每个 Thread 里面都有一个 ThreadLocalMap,而 ThreadLocalMap 中真正承载数据的是一个 Entry 数组,Entry 的 Key 是 threadlocal 对象的弱引用。

这里或许有的小伙伴有疑问,Entry 的 Key 是 threadlocal 对象的弱引用,为什么要用弱引用,换成强引用行不行?

首先,我们先了解一下什么是弱引用?

弱引用一般是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

实际开发中,当我们不需要 threadlocal 后,为了 GC 将 threadlocal 变量置为 null,没有任何强引用指向堆中的 threadlocal 对象时,堆中的 threadlocal 对象将会被 GC 回收,假设现在 Key 持有的是 threadlocal 对象的强引用,如果当前线程仍然在运行,那么从当前线程一直到 threadlocal 对象还是存在强引用,由于当前线程仍在运行的原因导致 threadlocal 对象无法被 GC,这就发生了内存泄漏。相反,弱引用就不存在此问题,当栈中的 threadlocal 变量置为 null 后,堆中的 threadlocal 对象只有一个 Key 的弱引用关联,下一次 GC 的时候堆中的 threadlocal 对象就会被回收,使用弱引用对于 threadlocal 对象而言是不会发生内存泄漏的

那么,第二个问题来了,是不是 Key 持有的是 threadlocal 对象的弱引用就一定不会发生内存泄漏呢?

结论是:如果你使用不当还是有可能发生内存泄露,但是,这里发生内存泄漏的地方和上面不同。

当 threadlocal 使用完后,将栈中的 threadlocal 变量置为 null,threadlocal 对象下一次 GC 会被回收,那么 Entry 中的与之关联的弱引用 key 就会变成 null,如果此时当前线程还在运行,那么 Entry 中的 key 为 null 的 Value 对象并不会被回收(存在强引用),这就发生了内存泄漏,当然这种内存泄漏分情况,如果当前线程执行完毕会被回收,那么 Value 自然也会被回收,但是如果使用的是线程池呢,线程跑完任务以后放回线程池(线程没有销毁,不会被回收),Value 会一直存在,这就发生了内存泄漏。

如何更好的降低内存泄漏的风险呢?

ThreadLocal 为了降低内存泄露的可能性,在 set,get,remove 的时候都会清除此线程 ThreadLocalMap 里 Entry 数组中所有 Key 为 null 的 Value。所以,当前线程使用完 threadlocal 后,我们可以通过调用 ThreadLocal 的 remove 方法进行清除从而降低内存泄漏的风险。

最后

如果大家想要实时关注我更新的文章以及我分享的干货的话,可以关注我的公众号 我们都是小白鼠

查看原文

赞 3 收藏 2 评论 0

Mulavar 发布了文章 · 2020-10-02

《Java并发编程的艺术》笔记—Java线程到底有几种状态?

1. Java线程的生命周期

粗略来看,Java线程的生命周期一共有5种不同的状态:

  • NEW:初始状态。刚被创建,还没有执行start()方法。
  • RUNNABLE:运行状态。由于JAVA的线程模型是一对一的内核级线程模型,所以线程内部实际还会有运行中和就绪两种状态,但从Java语言角度看统称为“运行态”。
  • WAITED:等待状态。进入该状态表示当前线程需要等待其他线程做出一些特定动作,根据是否设置超时时间可以细分为“等待“和”超时等待“两种状态。
  • BLOCKED:阻塞状态。表示线程在获取锁的过程中失败被阻塞
  • TREMINATED:终止状态。线程执行完毕。

而很多文章也会将等待和超时等待当做两个独立的状态,因此也会有Java线程有6种状态的说法,但本质都是一样的。

2. Java线程状态变迁

java线程在自身的生命周期中会随着代码执行在不同的状态之间进行切换,其状态变迁如下图所示:

image.png

状态变迁主要可以分为以下几类:

  • 初始 ==> 运行:创建好的线程调用start()方法进入运行状态
  • 运行 ==> 终止:线程执行完run()方法后结束
  • 运行 <==> 等待:当线程执行wait()方法或LockSupport的一些辅助方法, 会进入到等待状态。此时线程需要依靠其他线程通知才能返回到运行状态。JUC包Lock接口的阻塞实现均使用了LockSupport类的相关方法,因此调用JUC包中Lock接口实际进入的是等待状态而非阻塞状态
  • 运行 <==> 阻塞:线程在尝试进入synchronized关键字修饰的方法或代码块时获取锁失败会进入阻塞状态。

3. 等待态和阻塞态

在第二节里有一个状态没被提到:等待 ==> 阻塞。

其实实际情况下不会真的有这个状态的直接转换。当一个线程从等待态退出时,会先进入运行态,再尝试获取锁,锁竞争失败后进入阻塞态。该图特意用红色线画了一个"错误"的状态变迁是想提醒读者,等待态和阻塞态是两个完全不同的状态。

我们可以认为阻塞态是对锁的竞争,由JVM自动管理这些参与竞争的线程,当其中一个线程从临界区退出时,JVM会自动调度另一个竞争成功的线程进入临界区。而等待态则是一个线程需要等待另一个线程的通知,需要我们手动管理线程之间的通信。

4. 参考资料

Java并发编程的艺术
Java线程中等待态和阻塞态的区别

查看原文

赞 1 收藏 0 评论 0

Mulavar 收藏了文章 · 2020-10-02

阿里的秒杀系统是怎么设计的?

背景

我之前写过一个秒杀系统的文章不过有些许瑕疵,所以我准备在之前的基础上进行二次创作,不过让我决心二创秒杀系统的原因是我最近面试了很多读者,动不动就是秒杀系统把我整蒙蔽了,我懵的主要是秒杀系统的细节大家都不知道,甚至不知道电商公司一个秒杀系统的组成部分。

我之前在某电商公司就是做电商活动的,所以这样的场景和很多解决方案我是比较清楚的,那我就从我自身去带着大家看看一个秒杀的设计细节以及中间各种解决方案的利弊,以下就是我设计的秒杀系统,几乎涵盖了市面上所有秒杀的实现细节:

正文

首先设计一个系统之前,我们需要先确认我们的业务场景是怎么样子的,我就带着大家一起假设一个场景好吧。

我们现场要卖1000件下面这个婴儿纸尿裤,然后我们根据以往这样秒杀活动的数据经验来看,目测来抢这100件纸尿裤的人足足有10万人。(南极人打钱!)

你一听,完了呀,这我们的服务器哪里顶得住啊!说真的直接打DB肯定挂,但是别急嘛,有暖男敖丙在,任何系统我们开始设计之前我们都应该去思考会出现哪些问题?这里我罗列了几个非常经典的问题:

问题

高并发:

是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?

是吧,秒杀的特点就是这样时间极短瞬间用户量大

正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀

秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了,那这个数据随便搞个热销商品的秒杀可能都不止了。

大量的请求进来,我们需要考虑的点就很多了,缓存雪崩缓存击穿缓存穿透这些我之前提到的点都是有可能发生的,出现问题打挂DB那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发

超卖:

但凡是个秒杀,都怕超卖,我这里举例的只是尿不湿,要是换成100个MacBook Pro,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办? (没事看了敖丙的文章直接不怕)

那最后只能杀个开发祭天解气了,秒杀的价格本来就低了,基本上都是不怎么赚钱的,超卖了就恐怖了呀,所以超卖也是很关键的一个点。

恶意请求:

你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛...)肯定也知道的。

那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。

真实情况可能远远不止,因为机器请求的速度比人的手速往往快太多了,在贵州的敖丙我每年回家抢高铁票都是秒光的,我也不知道有没有黄牛的功劳,我要Diss你,黄牛。杰伦演唱会门票抢不到,我也Diss你。

Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,你还想看演唱会?还想回家?

不过不用黄牛我回家都难,我们云贵川跟我一样要回家过年的仔太多了555!

链接暴露:

前面几个问题大家可能都很好理解,一看到这个有的小伙伴可能会比较疑惑,啥是链接暴露呀?

相信是个开发同学都对这个画面一点都不陌生吧,懂点行的仔都可以打开谷歌的开发者模式,然后看看你的网页代码,有的就有URL,但是我写VUE的时候是事件触发然后去调用文件里面的接口看源码看不到,但是我可以点击一下查看你的请求地址啊,不过你好像可以对按钮在秒杀前置灰。

不管怎么样子都有危险,撇开外面的所有的东西你都挡住了,你卖这个东西实在便宜得过分,有诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。。。(开发:怎么TM又是我)

数据库:

每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404

反正不管你秒杀怎么挂,你别把别的搞挂了对吧,搞挂了就不是杀一个程序员能搞定的。

程序员:我TM好难啊!

问题都列出来了,那怎么设计,怎么解决这些问题就是接下去要考虑的了,我们对症下药。

我会从我设计的秒杀系统从上到下去给大家介绍我们正常电商秒杀系统在每一层做了些什么,每一层存在的问题,难点等。

我们从前端开始:

前端

秒杀系统普遍都是商城网页、H5、APP、小程序这几项。

在前端这一层其实我们可以做的事情有很多,如果用node去做,甚至能直接处理掉整个秒杀,但是node其实应该属于后端,所以我不讨论node Service了。

资源静态化:

秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

秒杀链接加盐:

我们上面说了链接要是提前暴露出去可能有人直接访问url就提前秒杀了,那又有小伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址比起页面人工点击的还是有很大优势

我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率大太多了,而且我可以一毫秒发送N次请求,搞不好你卖100个产品我全拿了。

那这种情况怎么避免?

简单,把URL动态化,就连写代码的人都不知道,你就通过MD5之类的摘要算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。

这个只能防止一部分没耐心继续破解下去的黑客,有耐心的人研究出来还是能破解,在电商场景存在很多这样的羊毛党,那怎么做呢?

后面我会说。

限流:

限流这里我觉得应该分为前端限流后端限流

物理控制:

大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。

这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。

这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。

按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。

你敢说你们秒杀的时候不是这样的?

前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。

后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。

Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。

我们卖1000件商品,请求有10W,我们不需要把十万都放进来,你可以放1W请求进来,然后再进行操作,因为秒杀对于用户本身就是黑盒的,所以你怎么做的他们是没感知的,至于为啥放1W进来,而不是刚好1000,是因为会丢掉一些薅羊毛的用户,至于怎么判断,后面的风控阶段我会说。

Nginx:

Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的Tomcat只能顶几百的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机

Tip:据我所知国内某大厂就是在去年春节活动期间租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量机来顶住压力。

这样一对比是不是觉得你的集群能顶很多了。

恶意请求拦截也需要用到它,一般单个用户请求次数太夸张,不像人为的请求在网关那一层就得拦截掉了,不然请求多了他抢不抢得到是一回事,服务器压力上去了,可能占用网络带宽或者把服务器打崩、缓存击穿等等。

风控

我可以明确的告诉大家,前面的所有措施还是拦不住很多羊毛党,因为他们是专业的团队,他们可以注册很多账号来薅你的羊毛,而且不用机器请求,就用群控,操作几乎跟真实用户一模一样。

那怎么办,是不是无解了?

这个时候就需要风控同学的介入了,在请求到达后端之前,风控可以根据账号行为分析出这个账号机器人的概率大不大,我现在负责公司的某些特殊系统,每个用户的行为都是会送到我们大数据团队进行分析处理,给你打上对应标签的。

那黑客其实也有办法:养号

他们去黑市买真实用户有过很多记录的账号,买到了还不闲着,帮他们去购物啥的,让系统无法识别他们是黑号还是真实用户的号。

怎么办?

通杀!是的没有办法,只能通杀了,通杀的意思就是,我们通过风管分析出来这个用户是真实用户的概率没有其他用户概率大,那就认为他是机器了,丢弃他的请求。

之前的限流我们放进来10000个请求,但是我们真正的库存只有1000个,那我们就算出最有可能是真实用户的1000人进行秒杀,丢弃其他请求,因为秒杀本来就是黑盒操作的,用户层面是无感知的,这样设计能让真实的用户买到东西,还可以减少自己被薅羊毛的概率。

风控可以说是流量进入的最后一道门槛了,所以很多公司的风控是很强的,蚂蚁金服的风控大家如果了解过就知道了,你的资金在支付宝被盗了,他们是能做到全款补偿是有原因的。

后端

服务单一职责:

设计个能抗住高并发的系统,我觉得还是得单一职责

什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式

也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。

单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(高可用)

Redis集群:

之前不是说单机的Redis顶不住嘛,那简单多找几个兄弟啊,秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群主从同步读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!

库存预热:

秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。

开发:你tm总算为我着想一次了。

那怎么办?

我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!

那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。

但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。

**多品几遍!!!**就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?

事务:

Redis本身是支持事务的,而且他有很多原子命令的,大家也可以用LUA,还可以用他的管道,乐观锁他也知支持。

限流&降级&熔断&隔离:

这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

消息队列(削峰填谷):

一说到这个名词,很多小伙伴就知道了,对的MQ,你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢?服务器挂了,程序员又要背锅的

秒杀就是这种瞬间流量很高,但是平时又没有流量的场景,那消息队列完全契合这样的场景了呀,削峰填谷。

Tip:可能小伙伴说我们业务达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码,至少以后公司体量上去了,别人一看居然不用改代码,一看代码作者是敖丙?有点东西!

你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。

数据库

数据库用MySQL只要连接池设置合理一般问题是不大的,不过一般大公司不缺钱而且秒杀这样的活动十分频繁,我之前所在的公司就是这样秒杀特卖这样的场景一直都是不间断的。

单独给秒杀建立一个数据库,为秒杀服务,表的设计也是竟可能的简单点,现在的互联网架构部署都是分库的。

至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节去康康)

分布式事务

这为啥我不放在后端而放到最后来讲呢?

因为上面的任何一步都是可能出错的,而且我们是在不同的服务里面出错的,那就涉及分布式事务了,但是分布式事务大家想的是一定要成功什么的那就不对了,还是那句话,几个请求丢了就丢了,要保证时效和服务的可用可靠。

所以TCC最终一致性其实不是很适合,TCC开发成本很大,所有接口都要写三次,因为涉及TCC的三个阶段。

最终一致性基本上都是靠轮训的操作去保证一个操作一定成功,那时效性就大打折扣了。

大家觉得不那么可靠的**两段式(2PC)三段式(3PC)**就派上用场了,他们不一定能保证数据最终一致,但是效率上还算ok。

总结

到这里我想我已经基本上把该考虑的点还有对应的解决方案也都说了一下,不知道还有没有没考虑到的,但是就算没考虑到我想我这个设计,应该也能撑住一个完整的秒杀流程。

最后大家再看看这个秒杀系统或许会有新的感悟,是不是一个系统真的没有大家想的那么简单,而且我还是有漏掉的细节,这是一定的。

秒杀这章我脑细胞死了很多,考虑了很多个点,最后还是出来了,忍不住给自己点赞

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

秒杀不一定是每个同学都会问到的,至少肯定没Redis基础那样常问,但是一旦问到,大家一定要回答到点上。

至少你得说出可能出现的情况需要注意的情况,以及对于的解决思路和方案,因为这才是一个coder的基本素养,这些你不考虑你也很难去进步。

最后就是需要对整个链路比较熟悉,注意是一个完整的链路,前端怎么设计的呀,网关的作用呀,怎么解决Redis的并发竞争啊,数据的同步方式呀,MQ的作用啊等等,相信你会有不错的收获。

不知道这是一次成功还是失败的二创,我里面所有提到的技术细节我都写了对应的文章,大家可以关注我去历史文章看看,天色已晚,我溜了。

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!

查看原文

Mulavar 赞了文章 · 2020-10-02

阿里的秒杀系统是怎么设计的?

背景

我之前写过一个秒杀系统的文章不过有些许瑕疵,所以我准备在之前的基础上进行二次创作,不过让我决心二创秒杀系统的原因是我最近面试了很多读者,动不动就是秒杀系统把我整蒙蔽了,我懵的主要是秒杀系统的细节大家都不知道,甚至不知道电商公司一个秒杀系统的组成部分。

我之前在某电商公司就是做电商活动的,所以这样的场景和很多解决方案我是比较清楚的,那我就从我自身去带着大家看看一个秒杀的设计细节以及中间各种解决方案的利弊,以下就是我设计的秒杀系统,几乎涵盖了市面上所有秒杀的实现细节:

正文

首先设计一个系统之前,我们需要先确认我们的业务场景是怎么样子的,我就带着大家一起假设一个场景好吧。

我们现场要卖1000件下面这个婴儿纸尿裤,然后我们根据以往这样秒杀活动的数据经验来看,目测来抢这100件纸尿裤的人足足有10万人。(南极人打钱!)

你一听,完了呀,这我们的服务器哪里顶得住啊!说真的直接打DB肯定挂,但是别急嘛,有暖男敖丙在,任何系统我们开始设计之前我们都应该去思考会出现哪些问题?这里我罗列了几个非常经典的问题:

问题

高并发:

是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?

是吧,秒杀的特点就是这样时间极短瞬间用户量大

正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀

秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了,那这个数据随便搞个热销商品的秒杀可能都不止了。

大量的请求进来,我们需要考虑的点就很多了,缓存雪崩缓存击穿缓存穿透这些我之前提到的点都是有可能发生的,出现问题打挂DB那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发

超卖:

但凡是个秒杀,都怕超卖,我这里举例的只是尿不湿,要是换成100个MacBook Pro,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办? (没事看了敖丙的文章直接不怕)

那最后只能杀个开发祭天解气了,秒杀的价格本来就低了,基本上都是不怎么赚钱的,超卖了就恐怖了呀,所以超卖也是很关键的一个点。

恶意请求:

你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛...)肯定也知道的。

那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。

真实情况可能远远不止,因为机器请求的速度比人的手速往往快太多了,在贵州的敖丙我每年回家抢高铁票都是秒光的,我也不知道有没有黄牛的功劳,我要Diss你,黄牛。杰伦演唱会门票抢不到,我也Diss你。

Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,你还想看演唱会?还想回家?

不过不用黄牛我回家都难,我们云贵川跟我一样要回家过年的仔太多了555!

链接暴露:

前面几个问题大家可能都很好理解,一看到这个有的小伙伴可能会比较疑惑,啥是链接暴露呀?

相信是个开发同学都对这个画面一点都不陌生吧,懂点行的仔都可以打开谷歌的开发者模式,然后看看你的网页代码,有的就有URL,但是我写VUE的时候是事件触发然后去调用文件里面的接口看源码看不到,但是我可以点击一下查看你的请求地址啊,不过你好像可以对按钮在秒杀前置灰。

不管怎么样子都有危险,撇开外面的所有的东西你都挡住了,你卖这个东西实在便宜得过分,有诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。。。(开发:怎么TM又是我)

数据库:

每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404

反正不管你秒杀怎么挂,你别把别的搞挂了对吧,搞挂了就不是杀一个程序员能搞定的。

程序员:我TM好难啊!

问题都列出来了,那怎么设计,怎么解决这些问题就是接下去要考虑的了,我们对症下药。

我会从我设计的秒杀系统从上到下去给大家介绍我们正常电商秒杀系统在每一层做了些什么,每一层存在的问题,难点等。

我们从前端开始:

前端

秒杀系统普遍都是商城网页、H5、APP、小程序这几项。

在前端这一层其实我们可以做的事情有很多,如果用node去做,甚至能直接处理掉整个秒杀,但是node其实应该属于后端,所以我不讨论node Service了。

资源静态化:

秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

秒杀链接加盐:

我们上面说了链接要是提前暴露出去可能有人直接访问url就提前秒杀了,那又有小伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址比起页面人工点击的还是有很大优势

我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率大太多了,而且我可以一毫秒发送N次请求,搞不好你卖100个产品我全拿了。

那这种情况怎么避免?

简单,把URL动态化,就连写代码的人都不知道,你就通过MD5之类的摘要算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。

这个只能防止一部分没耐心继续破解下去的黑客,有耐心的人研究出来还是能破解,在电商场景存在很多这样的羊毛党,那怎么做呢?

后面我会说。

限流:

限流这里我觉得应该分为前端限流后端限流

物理控制:

大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。

这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。

这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。

按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。

你敢说你们秒杀的时候不是这样的?

前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。

后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。

Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。

我们卖1000件商品,请求有10W,我们不需要把十万都放进来,你可以放1W请求进来,然后再进行操作,因为秒杀对于用户本身就是黑盒的,所以你怎么做的他们是没感知的,至于为啥放1W进来,而不是刚好1000,是因为会丢掉一些薅羊毛的用户,至于怎么判断,后面的风控阶段我会说。

Nginx:

Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的Tomcat只能顶几百的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机

Tip:据我所知国内某大厂就是在去年春节活动期间租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量机来顶住压力。

这样一对比是不是觉得你的集群能顶很多了。

恶意请求拦截也需要用到它,一般单个用户请求次数太夸张,不像人为的请求在网关那一层就得拦截掉了,不然请求多了他抢不抢得到是一回事,服务器压力上去了,可能占用网络带宽或者把服务器打崩、缓存击穿等等。

风控

我可以明确的告诉大家,前面的所有措施还是拦不住很多羊毛党,因为他们是专业的团队,他们可以注册很多账号来薅你的羊毛,而且不用机器请求,就用群控,操作几乎跟真实用户一模一样。

那怎么办,是不是无解了?

这个时候就需要风控同学的介入了,在请求到达后端之前,风控可以根据账号行为分析出这个账号机器人的概率大不大,我现在负责公司的某些特殊系统,每个用户的行为都是会送到我们大数据团队进行分析处理,给你打上对应标签的。

那黑客其实也有办法:养号

他们去黑市买真实用户有过很多记录的账号,买到了还不闲着,帮他们去购物啥的,让系统无法识别他们是黑号还是真实用户的号。

怎么办?

通杀!是的没有办法,只能通杀了,通杀的意思就是,我们通过风管分析出来这个用户是真实用户的概率没有其他用户概率大,那就认为他是机器了,丢弃他的请求。

之前的限流我们放进来10000个请求,但是我们真正的库存只有1000个,那我们就算出最有可能是真实用户的1000人进行秒杀,丢弃其他请求,因为秒杀本来就是黑盒操作的,用户层面是无感知的,这样设计能让真实的用户买到东西,还可以减少自己被薅羊毛的概率。

风控可以说是流量进入的最后一道门槛了,所以很多公司的风控是很强的,蚂蚁金服的风控大家如果了解过就知道了,你的资金在支付宝被盗了,他们是能做到全款补偿是有原因的。

后端

服务单一职责:

设计个能抗住高并发的系统,我觉得还是得单一职责

什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式

也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。

单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(高可用)

Redis集群:

之前不是说单机的Redis顶不住嘛,那简单多找几个兄弟啊,秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群主从同步读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!

库存预热:

秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。

开发:你tm总算为我着想一次了。

那怎么办?

我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!

那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。

但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。

**多品几遍!!!**就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?

事务:

Redis本身是支持事务的,而且他有很多原子命令的,大家也可以用LUA,还可以用他的管道,乐观锁他也知支持。

限流&降级&熔断&隔离:

这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

消息队列(削峰填谷):

一说到这个名词,很多小伙伴就知道了,对的MQ,你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢?服务器挂了,程序员又要背锅的

秒杀就是这种瞬间流量很高,但是平时又没有流量的场景,那消息队列完全契合这样的场景了呀,削峰填谷。

Tip:可能小伙伴说我们业务达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码,至少以后公司体量上去了,别人一看居然不用改代码,一看代码作者是敖丙?有点东西!

你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。

数据库

数据库用MySQL只要连接池设置合理一般问题是不大的,不过一般大公司不缺钱而且秒杀这样的活动十分频繁,我之前所在的公司就是这样秒杀特卖这样的场景一直都是不间断的。

单独给秒杀建立一个数据库,为秒杀服务,表的设计也是竟可能的简单点,现在的互联网架构部署都是分库的。

至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节去康康)

分布式事务

这为啥我不放在后端而放到最后来讲呢?

因为上面的任何一步都是可能出错的,而且我们是在不同的服务里面出错的,那就涉及分布式事务了,但是分布式事务大家想的是一定要成功什么的那就不对了,还是那句话,几个请求丢了就丢了,要保证时效和服务的可用可靠。

所以TCC最终一致性其实不是很适合,TCC开发成本很大,所有接口都要写三次,因为涉及TCC的三个阶段。

最终一致性基本上都是靠轮训的操作去保证一个操作一定成功,那时效性就大打折扣了。

大家觉得不那么可靠的**两段式(2PC)三段式(3PC)**就派上用场了,他们不一定能保证数据最终一致,但是效率上还算ok。

总结

到这里我想我已经基本上把该考虑的点还有对应的解决方案也都说了一下,不知道还有没有没考虑到的,但是就算没考虑到我想我这个设计,应该也能撑住一个完整的秒杀流程。

最后大家再看看这个秒杀系统或许会有新的感悟,是不是一个系统真的没有大家想的那么简单,而且我还是有漏掉的细节,这是一定的。

秒杀这章我脑细胞死了很多,考虑了很多个点,最后还是出来了,忍不住给自己点赞

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

秒杀不一定是每个同学都会问到的,至少肯定没Redis基础那样常问,但是一旦问到,大家一定要回答到点上。

至少你得说出可能出现的情况需要注意的情况,以及对于的解决思路和方案,因为这才是一个coder的基本素养,这些你不考虑你也很难去进步。

最后就是需要对整个链路比较熟悉,注意是一个完整的链路,前端怎么设计的呀,网关的作用呀,怎么解决Redis的并发竞争啊,数据的同步方式呀,MQ的作用啊等等,相信你会有不错的收获。

不知道这是一次成功还是失败的二创,我里面所有提到的技术细节我都写了对应的文章,大家可以关注我去历史文章看看,天色已晚,我溜了。

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!

查看原文

赞 114 收藏 81 评论 2

Mulavar 赞了文章 · 2020-09-29

Go语言中for range的"坑"

前言

Go 中的for range组合可以和方便的实现对一个数组或切片进行遍历,但是在某些情况下使用for range时很可能就会被"坑",下面用一段代码来模拟下:

func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i, v := range arr1 {
        arr2[i] = &v
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}

代码解析:

  1. 创建一个int slice,变量名为arr1并初始化 1,2,3 作为切片的值。
  2. 创建一个*int slice,变量名为arr2
  3. 通过for range遍历arr1,然后获取每一个元素的指针,赋值到对应arr2中。
  4. 逐行打印arr2中每个元素的值。

从代码上看,打印出来的结果应该是

1
2
3

然而真正的结果是

3
3
3

原因

因为for range在遍历值类型时,其中的v变量是一个的拷贝,当使用&获取指针时,实际上是获取到v这个临时变量的指针,而v变量在for range中只会创建一次,之后循环中会被一直重复使用,所以在arr2赋值的时候其实都是v变量的指针,而&v最终会指向arr1最后一个元素的值拷贝。

来看看下面这个代码,用for i来模拟for range,这样更易于理解:

func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    var v int
    for i:=0;i<len(arr1);i++ {
        v = arr1[i]
        arr2[i] = &v
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}

解决方案

  1. 传递原始指针
func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i := range arr1 {
        arr2[i] = &arr1[i]
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}
  1. 使用临时变量
func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i, v := range arr1 {
        t := v
        arr2[i] = &t
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}
  1. 使用闭包
func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i, v := range arr1 {
        func(v int){
             arr2[i] = &v
        }(v)
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}

官方提示

由于这一问题过于普遍,Golang甚至将其写入了文档的『常见错误』部分:文档

我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

wechat

查看原文

赞 5 收藏 2 评论 0

认证与成就

  • 获得 3 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-22
个人主页被 545 人浏览