2024年2月

image

介绍

相信对于Java面向对象部分,很多人很长一段时间对于接口和抽象类的区别,使用场景都不是很熟悉,同是作为抽象层重要的对象,工作中到底什么情况下使用抽象类,不是很清楚。本文就一次性把这些概念一次性说清楚,不用再烦恼了,哈哈!

核心概念

  1. 接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。

  2. java8开始增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。

  3. 抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。

  4. 有一条实际经验:尽可能地使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。

  5. 任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。

接口和抽象类的区别

接口和抽象类都是Java中定义行为的方式,但它们之间存在一些重要的区别。
image

  1. 定义与实现

接口
:接口是一种完全抽象的类型,它只包含抽象方法和常量。接口不能被实例化,只能被类实现。一个类可以实现多个接口。
抽象类
:抽象类是一个不完全的类,它可以包含抽象方法和非抽象方法。抽象类不能被直接实例化,需要通过子类来继承并实现所有抽象方法。一个类只能继承一个抽象类。

  1. 方法与变量

接口
:接口中只能定义抽象方法(从Java 8开始也可以定义默认方法和静态方法),所有方法默认都是public的。接口中定义的变量默认是public static final的。
抽象类
:抽象类中可以定义普通方法、抽象方法、静态方法、构造方法等,方法默认是public或protected的。抽象类中的变量可以是任何访问修饰符。

  1. 继承与实现

接口
:一个类可以实现多个接口,通过关键字implements。
抽象类
:一个类只能继承一个抽象类,通过关键字extends

代码示例

接口

package com.demo.java.test.javacore;

interface CanFight {
    void fight();
}

interface CanSwim {
    void swim();
}

interface CanFly {
    void fly();
}

class ActionCharacter {
    public void fight2(){
        System.out.println("ActionCharacter fighting");
    }
}

class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
    public void swim() {
        System.out.println("swiming");
    }
    
    public void fly() {
        System.out.println("flying");
    }

    @Override
    public void fight() {
        System.out.println("fighting");
    }
}

public class Adventure {
    public static void t(CanFight x) {
        x.fight();
    }
    
    public static void u(CanSwim x) {
        x.swim();
    }
    
    public static void v(CanFly x) {
        x.fly();
    }
    
    public static void w(ActionCharacter x) {
        x.fight2();
    }
    
    public static void main(String[] args) {
        Hero h = new Hero();
        t(h); // Treat it as a CanFight
        u(h); // Treat it as a CanSwim
        v(h); // Treat it as a CanFly
        w(h); // Treat it as an ActionCharacter
    }
}
  • 输出:
fighting
swiming
flying
ActionCharacter fighting

抽象类

以乐器类抽象举例,请看继承关系图

image

package com.demo.java.test.javacore;
// 音符枚举
enum Note{
    MIDDLE_C
}

/**
 *  乐器
 */
abstract class Instrument {
    private int i; // Storage allocated for each
    
    public abstract void play(Note n);
    
    public String what() {
        return "Instrument";
    }
    
    public abstract void adjust();
}

/**
 *  管乐器
 */
class Wind extends Instrument {
    @Override
    public void play(Note n) {
        System.out.println("Wind.play() " + n);
    }
    
    @Override
    public String what() {
        return "Wind";
    }
    
    @Override
    public void adjust() {
        System.out.println("Adjusting Wind");
    }
}

/**
 *  打击乐器
 */
class Percussion extends Instrument {
    @Override
    public void play(Note n) {
        System.out.println("Percussion.play() " + n);
    }
    
    @Override
    public String what() {
        return "Percussion";
    }
    
    @Override
    public void adjust() {
        System.out.println("Adjusting Percussion");
    }
}

/**
 *  有弦乐器
 */
class Stringed extends Instrument {
    @Override
    public void play(Note n) {
        System.out.println("Stringed.play() " + n);
    }
    
    @Override
    public String what() {
        return "Stringed";
    }
    
    @Override
    public void adjust() {
        System.out.println("Adjusting Stringed");
    }
}

/**
 *  铜管乐器
 */
class Brass extends Wind {
    @Override
    public void play(Note n) {
        System.out.println("Brass.play() " + n);
    }
    
    @Override
    public void adjust() {
        System.out.println("Adjusting Brass");
    }
}

class Woodwind extends Wind {
    @Override
    public void play(Note n) {
        System.out.println("Woodwind.play() " + n);
    }
    
    @Override
    public String what() {
        return "Woodwind";
    }
}

/**
 *  音乐组合类
 */
public class Music4 {
    static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    
    static void tuneAll(Instrument[] e) {
        for (Instrument i: e) {
            tune(i);
        }
    }
    
    public static void main(String[] args) {
        // 向上转型
        Instrument[] orchestra = {
            new Wind(),
            new Percussion(),
            new Stringed(),
            new Brass(),
            new Woodwind()
        };
        // 演凑
        tuneAll(orchestra);
    }
}
  • 输出:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

什么是循环依赖?

这个情况很简单,即A对象依赖B对象,同时B对象也依赖A对象,让我们来简单看一下。

// A依赖了B
class A{
 public B b;
}

// B依赖了A
class B{
 public A a;
}

这种循环依赖可能会引发问题吗?

在没有考虑Spring框架的情况下,循环依赖并不会带来问题,因为对象之间相互依赖是非常普遍且正常的现象。

比如

A a = new A();
B b = new B();

a.b = b;
b.a = a;

这样,A,B就依赖上了。

然而,在Spring框架中存在一个令人头疼的问题,即循环依赖,这一问题的根源是什么呢?

在Spring框架中,一个对象的实例化并非简单地通过
new
关键字完成,而是经历了一系列Bean生命周期的阶段。正是由于这种Bean的生命周期机制,才导致了循环依赖问题的出现。在Spring应用中,循环依赖问题是一个常见现象,有些情况下Spring框架能够自动解决这种问题,但在其他情况下,需要开发人员介入并进行手动解决。接下来将详细探讨这些情况。

要深入理解Spring中的循环依赖,首先需要对Spring中Bean的完整生命周期有所了解。在这里不会深入展开对Bean生命周期的详细描述,因为之前的文章已经单独探讨过这一话题。因此,这里将简要概述Bean生命周期的大致过程。

Spring 管理的对象称为 Bean,通过Spring的扫描机制获取到类的BeanDefinition后,接下来的流程是:

  1. 解析BeanDefinition以实例化Bean:
    a. 推断类的构造方法。
    b. 利用反射机制实例化对象(称为原始对象)。
  2. 填充原始对象的属性,实现依赖注入。
  3. 如果原始对象中的方法被AOP增强,生成代理对象:
    a. 根据原始对象生成代理对象。
  4. 将生成的代理对象存放到单例池(在源码中称为singletonObjects)中,以便下次直接获取。

这个过程简要描述了Spring容器在实例化Bean并处理AOP时的流程。

在Spring中,Bean的生成过程涉及多个复杂步骤,远不止上述简要提及的7个步骤。除了所列步骤外,还包括诸如Aware回调、初始化等繁琐流程,这些内容涉及的细节繁多,不在本文讨论范围。

在创建B类的Bean时,如果B类包含一个名为a的A类属性,那么在生成B的Bean时,需要确保A类的Bean已经存在。然而,由于B类Bean的创建条件是A类Bean的依赖注入过程,因此可能会导致循环依赖问题。这意味着在容器尝试实例化B类Bean时,它必须首先解决A类Bean的依赖关系,而A类Bean的实例化又依赖于B类Bean。所以这里就出现了循环依赖:

ABean创建-->依赖了B属性-->触发BBean创建--->B依赖了A属性--->需要ABean(但ABean还在创建过程中)

因此,这一系列问题最终导致无法成功创建ABean,进而也无法顺利创建BBean。

在这种循环依赖的情况下,正如前文所述,Spring通过一些机制来协助开发者解决部分循环依赖问题,这便是三级缓存。

三级缓存

在此简要介绍三级缓存的概念,随后在探讨AOP对象如何解决循环依赖问题时,将会对其进行更为详尽的回顾。

  • 在Spring框架中,单例对象缓存在
    singletonObjects
    中,其中存储的是已经经历了完整生命周期的bean对象。
  • earlySingletonObjects
    中的“early”一词表明其中缓存的是早期的bean对象。这里的“早期”指的是Bean的生命周期尚未完成,但已经将该Bean放入了
    earlySingletonObjects
    中。
  • singletonFactories
    中存储的是
    ObjectFactory
    ,即对象工厂,用于创建早期bean对象的工厂。

在前文的分析中,我们得知产生循环依赖问题的主要原因是Bean之间相互依赖,导致在创建Bean时出现了循环引用的情况。主要是:

A创建时--->需要B---->B去创建--->需要A,从而产生了循环

image

因此,我们现在将深入探讨为何缓存机制能够成功解决这种循环依赖难题。那么,如何打破这一循环呢?我们可以引入一个中间层(缓存)来化解。

image

在A的Bean创建过程中,在执行依赖注入之前,首先将A的原始Bean提前放入缓存中,这样一来,其他Bean在需要时可以直接从缓存中获取,随后才进行依赖注入操作。

在这种情况下,当A的Bean依赖于B的Bean时,如果B的Bean尚不存在,则必须启动B的Bean创建流程。这一流程与A的Bean创建过程相似,首先生成B的原始对象,然后将其提前暴露并置入缓存中。

接着,在对B的原始对象执行依赖注入A操作时,此时可以从缓存中检索A的原始对象(尽管这仅为A的原始对象,尚非最终Bean状态)。当B的原始对象完成依赖注入后,B的生命周期随之终结,从而也促使A的生命周期得以顺利结束。

在整个流程中,只存在一个A原始对象,因此对于B而言,即使在属性注入阶段将A原始对象注入,也并不会有任何影响,因为A原始对象在随后的生命周期中保持不变,未在堆中发生任何变化。

总结

在文章中详细探讨了循环依赖问题及其解决思路分析,揭示了Spring所提供的Bean创建过程并非如我们所想象的那样简单。这一过程涉及众多复杂步骤,因此Spring引入了缓存机制,通过在后续阶段逐步维护堆中的初始对象,并逐步进行赋值来逐步完成Bean的创建。这种缓慢而谨慎的方式确保了Bean的正确创建。

然而,这种处理方式仅适用于普通对象的创建。我们了解到,Spring还涉及另一个重要特性,即面向切面编程(AOP)。根据这一逻辑,AOP代理对象可能会遇到一些问题,这将在接下来的章节中进行深入讨论。这也解释了为何Spring需要三级缓存而不仅仅是二级缓存的原因。

前言

写在前面,让我们从 3 个问题开始今天的文章:什么是 Redis 缓存?它解决了什么问题?怎么使用它?

在笔者近 3 年的 Java 一线开发经历中,尤其是一些移动端、用户量大的互联网项目,经常会使用到 Redis 分布式缓存作为解决高并发的基本工具。但在使用过程中也有一些潜在的问题是必须要考虑的,比如:数据一致性、缓存穿透和雪崩、高可用集群等等。

下面我就将从关于缓存是什么、项目中的实现、数据一致性等这几个方面来分享一下我自己是怎么使用 Redis 实现分布式缓存的。


一、关于缓存

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束,且在多实例的情况下本地缓存不具有一致性。

而使用 Redis 或 memcached 之类的称为分布式缓存。在多实例(集群)的情况下,Redis 的事务会一次性、顺序性、排他性地执行队列中的一系列命令,各实例共用一份缓存数据,缓存具有一致性。

Redis 缓存处理请求

用户第一次访问数据库的数据,因为是从硬盘上读取的所以比较慢。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可,这里涉及到的数据一致性问题会在第四小节专门讲。

至于 Redis 为什么这么快,最主要有以下几个原因:

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,速度非常地快;
  2. 采用单线程,避免了不必要的上下文切换和竞争条件,不存在因多线程的切换而消耗 CPU;也不存在加锁、释放锁操作,也没有因死锁而导致的性能消耗;
  3. 使用多路 I/O 复用模型(很关键),非阻塞的 IO,能让单个线程高效地处理多个连接请求,尽量减少网络 IO 的时间消耗。
Redis 性能

注:X 轴为客户端连接数,Y 轴是 QPS。即在近一万的客户端连接下,还能达到近十万的 QPS,这样强悍的性能是 MySQL 无法企及的。


二、基本数据结构

众所周知,直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据(热点数据、读多写少的数据)转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

Redis 有 5 种基本的数据结构,具体参考我的另一篇博客:
https://www.cnblogs.com/CodeBlogMan/p/17816699.html


三、缓存注解

用于后端接口的数据缓存,加在接口的实现方法上,这是我在实际项目中处理高并发的基本做法之一。说到注解,那么就需要从以下几个必不可少的方面进行展开。

3.1自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Inherited
public @interface Cache {
    /**
     * 缓存 key 默认为空串,会根据调用的类签名自动生成
     * 如果指定 key,则使用指定的 key
     * @return redis key
     */
    String key() default "";

    /**
     * 超时时间,默认 3 秒
     * @return redis 的超时时间
     */
    int expiryTime() default 3;
}

3.2定义切点(拦截器)

  • 在 AOP 中,Joinpoint 代表了程序执行的某个具体位置,比如方法的调用、异常的抛出等。AOP 框架通过拦截这些 Joinpoint 来插入额外的逻辑,实现横切关注点的功能。
  • 而实现拦截器 MethodInterceptor 接口比较特殊,它会将所有的 @AspectJ 定义的通知最终都交给 MethodInvocation(子类 ReflectiveMethodInvocation )去执行。
public class CacheAnnotationInterceptor extends CacheAop implements MethodInterceptor {
    public CacheAnnotationInterceptor(ICache iCache) {
        // 调用父类有参构造
        super(iCache);
    }
    public CacheAnnotationInterceptor(RedisTemplate<Object, Object> redisTemplate) {
        // 调用父类有参构造
        super(redisTemplate);
    }
    /**
     * 反射实现,通过拦截方法的执行来实现通知的效果
     * @param methodInvocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 即下面父类的具体 AOP 实现
        return super.cacheAop(methodInvocation);
    }
}

3.3 AOP 实现

下面的 AOP 仅是大致过程,思路用注释写得比较清楚了,完整的代码有时间脱敏后再分享吧。

/**
 * 子类 CacheAnnotationInterceptor 重写了 MethodInterceptor 的 invoke() 方法
 */
public class CacheAop {
    /**
     * 基于 Redis 的一些常见 API 实现
     */
    protected ICache iCache;
    protected RedisTemplate<Object, Object> redisTemplate;
    public CacheAop(ICache iCache) {
        this.iCache = iCache;
    }
    public CacheAop(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    /**
     * 缓存切面实现
     */
    public Object cacheAop(MethodInvocation methodInvocation) throws Throwable {
        // 检查是否使用缓存
        if (this.isUseCache(methodInvocation)) {
            // 自定义缓存注解
            Cache cache = methodInvocation.getMethod().getAnnotation(Cache.class);
            // 生成缓存的 key
            String key = this.generateCacheKey(cache, methodInvocation);
            // 缓存操作
            return this.handlerCache(key, cache, methodInvocation);
        } else {
            // 需要执行的时候,调用.proceed()方法即可
            return methodInvocation.proceed();
        }
    }    
}

3.4使用示例

下面是一个简单示例,@Cache 注解加在需要缓存的方法上,设置过期时间为 5 秒。即 5 秒内调用该方法,返回的数据是来自缓存,过期后会再次从数据库中获取,并重新写入缓存,循环往复。

    /**
     * 根据 Id 从缓存中查询详情
     * @param id
     * @return
     */
    @Cache(expiryTime = 5)
    public Study getDetailByIdFromCache(String id) {
        return Optional.of(this.studyMapper.selectById(id)).orElse(null);
    }


四、数据一致性

数据库一旦引入了其它组件,必然会带来数据一致性的问题。这里不考虑强一致性,因为强一致性引发的性能问题在高并发的情景下是不可接受的。所以只考虑最终一致性。

4.1缓存更新策略

一般来说,为了保证数据一致性,会有 3 种常见的 Redis 缓存更新策略,如下表所示:

策略 描述 一致性 维护成本
内存淘汰 无需自己维护,Redis 自己有内存淘汰机制,当内存不足时会淘汰部分,下次查询时再更新缓存。
超时剔除 为缓存数据添加生存时间 TTL(Time To Live),到期后自动删除缓存,下次查询时再更新缓存。 一般
主动更新 额外编写代码逻辑,在修改数据库数据的同时,更新缓存。

我自己在项目是选择
超时剔除

主动更新
这 2 种方法搭配使用的,在合适的时候用合适的办法。

前者适合在对实时性要求不那么高的情况下使用,后者适合在对实时性要求较高的场景使用。至于内存淘汰是不可能会用的,太傻瓜了,放到线上 100% 出问题。

超时剔除的核心逻辑:
缓存来源于数据库,到过期时间后,缓存中的数据会被删除;用户再次请求的就是数据库的数据,再把数据库数据重新放入到缓存,依次反复。

主动更新的核心逻辑:
缓存操作一定不能和数据库事务作为一个大事务来处理,尤其是在较复杂的业务流程中,一般都是先完成数据库的事务操作后,再去操作缓存中的数据。

4.2缓存读写过程

具体读和写分为以下两点:

  • 对于读操作,一般都是先读取缓存,如果命中则返回;没有命中则去读数据库返回数据,这个逻辑很好理解,也没什么争议。

  • 对于写操作,有两个需要考虑的问题:


    1. 如何更新缓存,是删除缓存还是修改缓存?

      答:设置过期时间,直接删。
      不必要再去修改之前的缓存数据

    2. 是先更新数据库还是先更新缓存?

      答:先更新数据库,再更新缓存


      • 如果更新完了数据库,但是之前的缓存删除失败,读的依然是旧数据怎么办?

        答:设置较短的缓存时间,短时间内再次删除缓存。

      • 如果数据库是主从结构,即 master 负责事务操作,slave 只负责读,数据同步的延迟影响到缓存的更新怎么办?

        答:考虑从硬件下手,提升数据库性能 + 提升网络带宽。


五、高可用

5.1缓存穿透

缓存穿透:
是指客户端请求的数据在缓存中和数据库中都不存在,那么缓存永远不会生效。
这样,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。此时,缓存起不到保护后端持久层的意义,就像被穿透了一样。

以下是常用的缓存穿透的解决方案:

  • 对空值进行缓存:即使一个查询返回的数据为空,仍然把这个空结果(null)进行缓存,同时还可以对空结果设置较短的过期时间。这种方法实现简单,维护方便,但是会额外的内存消耗。

  • 采用布隆过滤器:(布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

  • 加强 id 复杂度(如雪花和 UUID)和参数校验,比如保证要查询的 key 不为负数或者非法字符串

  • 加强用户权限校验:对页面操作加以限制,对接口的调用进行鉴权。

5.2缓存击穿

缓存击穿:
是指某个热点 key,在缓存过期的一瞬间有大量的请求进来
,由于此时缓存刚好过期,所以请求最终都会走数据库,数据库压力瞬间骤增,导致数据库存在被打挂的风险。这样的情况下,彷佛缓存被请求给击穿了。

应该这种情况主要的解决方案如下:

  • 加互斥锁。当热 key 过期大量的请求涌入时,只有第一个请求能获取锁并阻塞,此时保证该请求查询数据库,并将查询结果写入 redis 缓存后释放锁,则后续的请求直接走缓存。

以下是一个通用解决方案的大致思路,可以兼顾处理缓存穿透和击穿问题。至于下面提到的“给 Key 设置合理的 TTL 并加上随机值”,这个也比较好实现。

    protected Object handleCache(String key, Cache cache, MethodInvocation invocation) throws Throwable {
        //设置 key 到 ThreadLocal 中,方便使用
        KeyThreadLocalUtils.setValue(key);
        //双重检查,防止高并发情况下因为缓存失效导致的缓存穿透问题
        Object value = this.getValueForCache(key);
        if (Objects.isNull(value)) {
            //加锁,防止缓存击穿
            synchronized (DigestUtils.md5Hex(key).intern()) {
                logger.info("方法为:{},key为:{}", invocation.getMethod().getName(), key);
                value = this.getValueForCache(key);
                //对空值也进行缓存
                if (Objects.isNull(value)) {
                    value = invocation.proceed();
                    if (value instanceof Serializable) {
                        this.setValueToCache(key, value, cache.expiryTime());
                    } else {
                        logger.warn("方法{}使用了缓存注解,但返回对象{}未序列化", invocation.getMethod().getName(), value);
                    }
                }
            }
        }
        KeyThreadLocalUtils.removeValue();
        return value;
    }

5.3缓存雪崩

缓存雪崩:
是指由于大量的缓存 key 的过期时间相同,导致数据在同一时刻集体失效,或者 Redis 服务宕机
,导致大量请求到达数据库,给数据库带来巨大压力。这种情况通常是由于
缓存时间设置不当
,或者
缓存容量不足
引起的。

以下是常用的缓存雪崩的解决方案:

  • 给 Key 设置合理的 TTL 并加上随机值

  • 增加缓存容量

  • 给缓存业务添加降级限流策略

5.4Redis 集群

在硬件层面,通过购买云服务厂商的 Redis 集群来保证服务的高可用。以下拿阿里云云数据库 Redis 的一些基本配置来举例:

阿里云云数据库 Redis


六、文章小结

到这里关于使用 Redis 实现分布式缓存的全过程就分享完了,其实关于 redis 缓存的高可用部分还有许多能详细展开的地方。但是目前我对于缓存的击穿、穿透和雪崩没有太多的实际场景来分享,更多的是一种学习和储备。

最后,如果文章有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

RedLock 是一种分布式锁的实现算法,由 Redis 的作者 Salvatore Sanfilippo(也称为 Antirez)提出,主要用于解决在分布式系统中实现可靠锁的问题。在 Redis 单独节点的基础上,RedLock 使用了多个独立的 Redis 实例(通常建议是奇数个,比如 5 个),共同协作来
提供更强健的分布式锁服务

RedLock 算法旨在解决单个 Redis 实例作为分布式锁时可能出现的单点故障问题,通过在多个独立运行的 Redis 实例上同时获取锁的方式来提高锁服务的可用性和安全性。

RedLock 具备以下主要特性:

  1. 互斥性
    :在任何时间,只有一个客户端可以获得锁,确保了资源的互斥访问。
  2. 避免死锁
    :通过为锁设置一个较短的过期时间,即使客户端在获得锁后由于网络故障等原因未能按时释放锁,锁也会因为过期而自动释放,避免了死锁的发生。
  3. 容错性
    :即使一部分 Redis 节点宕机,只要大多数节点(即过半数以上的节点)仍在线,RedLock 算法就能继续提供服务,并确保锁的正确性。

1.RedLock 实现思路

RedLock 是对集群的每个节点进行加锁,如果大多数节点(N/2+1)加锁成功,则才会认为加锁成功。

这样即使集群中有某个节点挂掉了,因为大部分集群节点都加锁成功了,所以分布式锁还是可以继续使用的。

2.工作流程

RedLock 算法的工作流程大致如下:

  • 客户端向多个独立的 Redis 实例尝试获取锁,设置锁的过期时间非常短。
  • 如果客户端能在大部分节点上成功获取锁,并且所花费的时间小于锁的过期时间的一半,那么认为客户端成功获取到了分布式锁。
  • 当客户端完成对受保护资源的操作后,它需要向所有曾获取锁的 Redis 实例释放锁。
  • 若在释放锁的过程中,客户端因故无法完成,由于设置了锁的过期时间,锁最终会自动过期释放,避免了死锁。

3.基本使用

在 Java 开发中,可以使用 Redisson 框架很方便的实现 RedLock,具体操作代码如下:

import org.redisson.Redisson;
import org.redisson.api.RedisClient;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.redisson.RedissonRedLock;

public class RedLockDemo {

    public static void main(String[] args) {
        // 创建 Redisson 客户端配置
        Config config = new Config();
        config.useClusterServers()
        .addNodeAddress("redis://127.0.0.1:6379",
                        "redis://127.0.0.1:6380",
                        "redis://127.0.0.1:6381"); // 假设有三个 Redis 节点
        // 创建 Redisson 客户端实例
        RedissonClient redissonClient = Redisson.create(config);
        // 创建 RedLock 对象
        RedissonRedLock redLock = redissonClient.getRedLock("resource");
        try {
            // 尝试获取分布式锁,最多尝试 5 秒获取锁,并且锁的有效期为 5000 毫秒
            boolean lockAcquired = redLock.tryLock(5, 5000, TimeUnit.MILLISECONDS); 
            if (lockAcquired) {
                // 加锁成功,执行业务代码...
            } else {
                System.out.println("Failed to acquire the lock!");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Interrupted while acquiring the lock");
        } finally {
            // 无论是否成功获取到锁,在业务逻辑结束后都要释放锁
            if (redLock.isLocked()) {
                redLock.unlock();
            }
            // 关闭 Redisson 客户端连接
            redissonClient.shutdown();
        }
    }
}

4.实现原理

Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。

RedissonMultiLock 是 Redisson 提供的一种分布式锁类型,它可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。这样可以保证多个锁的一致性。

RedissonMultiLock 使用示例如下:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.multi.MultiLock;

public class RedissonMultiLockDemo {

    public static void main(String[] args) throws InterruptedException {
        // 创建 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // 创建多个分布式锁实例
        RLock lock1 = redisson.getLock("lock1");
        RLock lock2 = redisson.getLock("lock2");
        RLock lock3 = redisson.getLock("lock3");

        // 创建 RedissonMultiLock 对象
        MultiLock multiLock = new MultiLock(lock1, lock2, lock3);

        // 加锁
        multiLock.lock();
        try {
            // 执行任务
            System.out.println("Lock acquired. Task started.");
            Thread.sleep(3000);
            System.out.println("Task finished. Releasing the lock.");
        } finally {
            // 释放锁
            multiLock.unlock();
        }
        // 关闭客户端连接
        redisson.shutdown();
    }
}

在示例中,我们首先创建了一个 Redisson 客户端并连接到 Redis 服务器。然后,我们使用 redisson.getLock 方法创建了多个分布式锁实例。接下来,我们通过传入这些锁实例来创建了 RedissonMultiLock 对象。

说回正题,RedissonRedLock 是基于 RedissonMultiLock 实现的这点,可以从继承关系看出。

RedissonRedLock 继承自 RedissonMultiLock,核心实现源码如下:

public class RedissonRedLock extends RedissonMultiLock {
    public RedissonRedLock(RLock... locks) {
        super(locks);
    }

    /**
     * 锁可以失败的次数,锁的数量-锁成功客户端最小的数量
     */
    @Override
    protected int failedLocksLimit() {
        return locks.size() - minLocksAmount(locks);
    }

    /**
     * 锁的数量 / 2 + 1,例如有3个客户端加锁,那么最少需要2个客户端加锁成功
     */
    protected int minLocksAmount(final List<RLock> locks) {
        return locks.size()/2 + 1;
    }

    /** 
     * 计算多个客户端一起加锁的超时时间,每个客户端的等待时间
     */
    @Override
    protected long calcLockWaitTime(long remainTime) {
        return Math.max(remainTime / locks.size(), 1);
    }

    @Override
    public void unlock() {
        unlockInner(locks);
    }
}

从上述源码可以看出,Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的,当 RedLock 是对集群的每个节点进行加锁,如果大多数节点,也就是 N/2+1 个节点加锁成功,则认为 RedLock 加锁成功。

5.存在问题

RedLock 主要存在以下两个问题:

  1. 性能问题
    :RedLock 要等待大多数节点返回之后,才能加锁成功,而这个过程中可能会因为网络问题,或节点超时的问题,影响加锁的性能。
  2. 并发安全性问题
    :当客户端加锁时,如果遇到 GC 可能会导致加锁失效,但 GC 后误认为加锁成功的安全事故,例如以下流程:
    1. 客户端 A 请求 3 个节点进行加锁。
    2. 在节点回复处理之前,客户端 A 进入 GC 阶段(存在 STW,全局停顿)。
    3. 之后因为加锁时间的原因,锁已经失效了。
    4. 客户端 B 请求加锁(和客户端 A 是同一把锁),加锁成功。
    5. 客户端 A GC 完成,继续处理前面节点的消息,误以为加锁成功。
    6. 此时客户端 B 和客户端 A 同时加锁成功,出现并发安全性问题。

6.已废弃的 RedLock

因为 RedLock 存在的问题争议较大,且没有完美的解决方案,所以 Redisson 中已经废弃了 RedLock,这一点在 Redisson 官方文档中能找到,如下图所示:

课后思考

既然 RedLock 已经被废弃,那么想要实现分布式锁,同时又想避免 Redis 单点故障问题,应该使用哪种解决方案呢?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

23年考完PMP和MBA后感觉自己还需要在职称方面努努力,于是乎开始了高项的备考。幸不辱命,第一次考试就过了,我准备论文是用的自己实际的项目经验,且为甲方的视角并加入了部分敏捷,非常独特,在软考中比较少见,脱离了千篇一律的模式,论文比较难挂,本篇是个人对论文的总结,AAA 、AA、A指的是2023年下半年我觉得会出题的概率,可忽略。

论文总结如下:

开头及结尾:

2022

9
月,我作为
XX
规划设计院的信息化经理,负责了院内
OA
系统的建设工作。该项目总合同金额为
142
万元(其中主合同为
132
万元,补充合同为
9
万元,不含服务器费用),建设工期
6
个月,我在项目中担任甲方项目经理的角色,带领乙方实施工程师、测试工程师、开发工程师和产品经理完成系统建设任务。该项目包含行政办公、固定资产、审计管理、公文管理、图档管理、经营管理、项目管理、费控等主要模块,旨在建设一套专业的
OA
系统来支撑公司的流程管理体系。本文以该项目为例,讨论了信息系统项目建设过程中的
XX
管理,主要从
.......
过程进行阐述。


2022

6
月,我和公司领导为项目编写了项目建议书,其中对项目建设的必要条件做出了一定要求。然而,公司本身缺乏
OA
开发经验,且又希望在
OA
的建设过程中学习成熟的流程管理模式,识别最佳实践并标准化业务流程,所以公司决定通过外部采购来满足公司的需求。经过招标过程中的















等环节,
XX
软件公司以
132
万元的价格中标。另外,参考中标人提供的运行环境建议
·
,我们采购了三台腾讯云服务器,三年服务期共计
19
万元。系统采用
Java

Spring Cloud
架构,数据库用的是
MySQL
,另外使用
docker
独立部署文件存储和
ES
全文检索服务。考虑到本系统既有通用的模块,又有需要定制化开发且前期难以确定的模块,所以我们在此项目中采用了

以预测型为主、敏捷型为辅的混合型开发方法

尽管该项目的技术复杂度不高,但系统外要与
6
个系统对接接口,内要与
7
个部门沟通协调,业务极其繁琐。所以,在项目中需要格外注意细节,特别是要严格把控
XX
的管理,在本文中,我将特别围绕项目的
XX
管理进行论述。

..........

在团队的共同努力下,系统成功上线试运行,
1
个月后运行趋于稳定,于
2023

3
月如期验收,对
XX
管理的高度重视为项目的成功打下了坚实的基础。在这个项目中我学到了很多经验,也获得了不少的心得和感悟。

首先,甲方的信息化岗需要强烈的责任感,不然啥事都扔给乙方,自己当个甩手掌柜岂不美哉?然而,要做出让绝大部分员工都满意的系统,需要信息化管理者不断求索,严格认真的把控项目
的每一个过程。其次
,我深刻认识到一个企业若想要长久发展,必须要建立良好的制度和流程,企业信息化最大的问题不在于技术也不在于信息系统,而在于流程制度的混沌和人的无序。我认为信息化的关键在于利用信息化过程来规范制度流程并管理人的行为,从而提升综合管理能力,增强企业的竞争力。

在未来的工作中,我将继续推进企业的信息化转型,为我国信息化建设添砖加瓦。

AAA
整合:

一、制定项目章程

项目章程是公司高层下达一份正式批准项目的文件,是对项目经理正式授权的过程。
前期两个多月的项目招投标工作完成后,我和院领导根据
合同

招标文件
,一起制定了项目章程,最后由领导在项目启动会上正式签发,授权我担任本项目的项目经理。该章程包括
项目目的、项目背景、高层级的需求、整体预算、总体里程碑进度、主要可交付成果
等内容

为项目的顺利实施奠定了良好的基础

二、制定项目管理计划

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何整合所有管理计划。我带领项目团队以
项目章程

其他规划过程的输出的子计划
为依据

联合项目主要干系人,多次召开会议,共同讨论
编制了
项目管理计划
。这份计
划除了各类子管理计划和基准之外,还规定了本项目采用以
预测型为主、敏捷为辅的混合型的开发方法
,对于固定资产、公文管理等通用模块我们使用预测型的开发方法,对于前期难以预料的定制化开发模块,如安全生产、费控等模块,我们采用敏捷开发。

三、指导与管理项目工作

有了完善的管理计划,更应按计划贯彻执行
。指导和管理项目工作是领导团队完成
可交付成果
过程。
在项目实施过程中我非常重视对项目跟踪,及时了解项目的进展情况,记录项目遇到的各种问题,使用腾讯在线文档编制成一份
问题日志
。另外,定期与用户、公司高层、相关设备供应商等干系人进行必要沟通,交流项目进展中存在的问题,收集他们对项目的意见和建议,添加到问题日志中。

敏捷
...

四、管理项目知识

管理项目知识是一个在整个项目中持续开展,不断积累、分享和学习显性知识和隐形知识的过程。对于调研和项目运行过程中遇到的比较繁琐的业务知识、使用
IPAD
看板、
teambition
等工具进行知识分享,让每个人都能看到业务的具体解决思路。并且把总结到的经验教训汇总成经验教训登记册。每隔一段时间,就会开一次知识分享会,在会议上分享各种知识,总结经验教训,持续更新到

经验教训登记册

五、监控项目工作

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误,监控项目工作在整个项目过程中必不可少。具体而言,我要求项目成员每周提供项目周报,每周周五下午开展周例会对当前项目的情况进行评估和分析
,结合在项目过程中收集到的各种
工作绩效信息
,对项目当前的进度快慢、成本效益进行分析,最终形成了一份
工作绩效报告

另外在敏捷的开发阶段,我通过
10
分钟的

每日站会
,依次汇报昨天干了什么,以及今天的工作计划,了解项目的情况,使用
看板

迭代燃尽图
监控项目过程中,本次迭代任务还剩下多少个功能点待完成,控制项目进度在预定的轨道上运行。

六、实施整体变更控制

项目实施的过程中,各种类型的变更是不可避免的,实施整合变更控制就是在项目中,有变更的时候,要严格遵守变更控制程序,谨防随意变更。比如在项目阶段前期,办公室领导对公文管理赠送的在线文档编辑功能十分不满意,认为其产品老旧,兼容性差。我进行了初步评估,测试后确实产品一般,但胜在免费。经过和乙方的沟通后提议使用
WPS
在线文档编辑插件代理原来的插件。经过院领导(
CCB
)的同意后,通知干系人结果并签订补充合同
9
万元,采购了
WPS
在线文档编辑服务。办公室领导对新的插件十分满意。

然而在
敏捷开发部分
,我们拥抱变更,不再走变更流程,把用户的需求转化为一个个用户故事,加入产品待办事项列表里并进行优先级排序。在每两周的迭代计划会议上选取优先级最高的用户故事加入迭代事项列表中。

七、结束项目或阶段

结束项目或阶段是完结所有项目管理过程的所有活动,以正式结束项目的过程。
2023

2
月,项目成功上线试运行,在运行一个月后,我们组织业务专家、公司高层领导和人事、财务、综合办公室等部门负责人,以及乙方项目团队召开了项目验收会议,

确保所有工作完结
,经我方评审后一致通过验收,确认签字。同时,我整理和归档了项目中的各式文档,总结项目的经验教训,更新组织过程资产,加入到公司的知识库当中。

AAA
资源:

一、规划资源管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何进行资源管理工作。
项目规划阶段,
我带领项目团队以
项目管理计划

项目章程
为依据,联合项目主要干系人,多次召开会议,讨论
编制了
资源管理计划

团队章程

为项目的顺利实施奠定了良好的基础

在资源管理计划中,我采用
RACI
矩阵

对项目团队成员的职责进行了明确,当然在记录团队成员的角色与职责时,我们特别注意每项工作有且仅有
1
名负责人,以免出现职责不明的情况。

二、估算活动资源:

估算活动资源是估算完成项目目标所需资源种类、数量、特性的过程。本项目中,我组织相关会议,邀请所有团队成员及重要干系人,以
资源管理计划、范围基准
为依据,在会议上通过类比过去同类型的项目的方式估计本项目所需资源,最终形成了资源需求、估算依据和
资源分解结构(
RBS

。例如:该项目需要
1
个测试、
2
个开发、
1
个实施、
1
个产品,还需要云服务器
3
台,办公用笔记本
4
台,单位食堂饭卡
4
张。

三、获取资源

有了完善的资源管理计划,更应按计划贯彻执行。
获取资源是获取项目所需的人员、设备和材料等资源的过程。本项目在早期的招标文件中乙方已经
预分派
了项目团队成员。对于需要的服务器设备,我方根据前期和乙方沟通的资源需求,采购了
3
台腾讯云服务器三年使用权,共计
19
万元。我通过一系列的沟通和协调,完成了其他资源的合理调配,最终形成了

项目团队派工单、物质资源分配单

资源日历
等文件。

四、建设团队

建设团队是提高团队技能,加强团队协作,改进团队氛围,进而提高团队绩效的过程。本项目主要
集中办公
的方式,让项目团队处在同一地点办公,强化团队的沟通协调。然而却因为新冠疫情的存在,部分团队成员时不时会被封锁在家,所以我们也会组成
虚拟团队
,使用
IPAD
看板、在线文档、腾讯会议等工具进行线上沟通交流,完成项目的建设。另外,我为乙方项目团队申请了工作餐,使他们可以在我方单位食堂免费就餐,也不定期组织团队一起吃个饭交流一下感情,团队很快就从

震荡阶段
进入到了
规范阶段

五、管理团队

管理团队实际上就是一个
解决问题

管理冲突
的过程。
在项目管理过程中,冲突总是不可避免的,但是只要有好的解决方法,坏事也能变好事。一般而言,解决冲突的方法有
合作
/
解决、缓和
/
包容、撤退
/
回避、妥协
/
调解、强迫
/
命令

,我更喜欢使用合作
/
解决的方法来管理冲突。例如有一次,产品经理和开发就固定资产标签打印问题争执不下,产品经理认为,对接打印机打印需要的标签格式是一个很简单的需求,最多用
3
天时间解决,然而开发人员却说打印机太过老旧,对接起来难度太大,会影响项目进度。我了解到这个他们冲突的根源之后,询问了一下同学群里有没有写过类似对接打印机接口的代码,最后群策群力了解到这个东西其实压根不需要对接打印机的后端接口,因为打印机的编码使用的是国际标准
128 Code
算法生成的条码,直接使用这个算法生成图片,不需要对接接口也行。最后开发和产品经理觉得这个方案既简单又省时,都欣然接受这个方案。

六、控制资源

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误

控制资源过程在整个项目生命周期中必不可少。
具体而言,我要求项目成员每周提供项目周报,每周周五下午开展周例会对当前项目的情况进行评估和分析,
工作绩效数据
转化为
工作绩效信息
,并使项目一直行驶在规定的轨道上。然而总会有脱轨的时候
,比如在项目阶段前期,办公室领导对公文管理赠送的在线文档编辑功能十分不满意,认为其产品老旧,兼容性差。我进行了初步评估,测试后确实产品一般,但胜在免费。经过和乙方的沟通后提议使用
WPS
在线文档编辑插件代理原来的插件。经过院领导(
CCB
)的同意后,通知干系人结果并签订补充合同
9
万元,采购了
WPS
在线文档编辑服务。办公室领导对新的插件十分满意。

然而在
敏捷开发部分
,我们拥抱变更,不再走变更流程,把用户的需求转化为一个个用户故事,加入产品待办事项列表里并进行优先级排序。在每两周的迭代计划会议上选取优先级最高的用户故事加入迭代事项列表中。

AA
采购:

一、规划采购管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何进行采购管理工作。
我带领项目团队以
项目管理计划

项目章程
为依据,联合项目主要干系人,多次召开会议,讨论
编制了
采购管
理计划

采购工作说明书

为项目的顺利实施奠定了良好的基础。该计划包含采购产品、采购方法、采购数量、采购时间等详细内容,除了
OA
系统还有云服务器的购买(由中标人给出系统运行环境后再决定)等采购工作。公司

本身缺乏
OA
开发经验,又希望在
OA
的建设过程中学习成熟的流程管理模式,识别最佳实践并标准化业务流程,经过院领导、信息部和采购部的多轮商议,最终决定通过外部采购一套专业的
OA
系统来满足公司的需求。

本项目项目初步估算的成本已达
100
万元以上,按照院内和集团的管理办法,必须采用公开招标的方式进行招标。我与采购部积极合作,联系了招标代理机构
-
深圳市
XX
采购网,招标代理机构参考我们的项目需求和建议制定了

招标文件
,该文件包含五大内容,分别是投标人须知、评标方法(综合评标法)、主要合同条框和格式、项目需求及投标文件的编制格式。根据代理机构的建议,综合评标法使用的评分标准为:技术标
60%
,价格标
30%
、商务标
10%

二、实施采购

有了完善的管理计划,更应该按计划执行。
实施采购主要的工作是获取选定的卖方并完成采购的过程。本项目中,我们以
采购管理计划、采购工作说明书和采购文件
为依据,经过















四个流程,最终选定了中标人签署合同。




,根据有关规定,公开招标的文件必须在两个以上网站或者媒体上发布,我们准备好了预先制定的招标文件,编制了招标公告,发表在了公司的官方门户网站以及招标代理机构的深圳市
XX
采购网上。




,经过
20
天有条不紊的招标工作,共有
5
个投标人投标,校验资质后有
1
个投标人不符合,其余
4
个符合资质。




,该环节主要是招标代理机构代理开标,也由他们组织评标委员会,评标委员会有
5
位,其中含技术专家
3
位。当天开标人开标,唱标人唱标、投标人述标、评标委员会评标,最终得出了按得分依次排序的三位中标候选人。




,最后虽然有三位中标候选人,但是只能和得分顺位第一的中标人讨论项目需求,均对项目情况无异议,然后对合同条款进行多次修改,均确认无误后签订了合同。

三、控制采购

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误。控制采购在整个项目生命周期中必不可少。具体而言,我要求项目组成员每周提交项目周报,并且在每周五下午开展周例会对项目的情况进行评估和分析,把
工作绩效数据
转化为
工作绩效信息
,并使项目一直行驶在正确的轨道上。然而总会有脱轨的时候
,比如在项目阶段前期,办公室领导对公文管理赠送的在线文档编辑功能十分不满意,认为其产品老旧,兼容性差。我进行了初步评估,测试后确实产品一般,但胜在免费。经过和乙方的沟通后提议使用
WPS
在线文档编辑插件代理原来的插件。经过院领导(
CCB
)的同意后,通知干系人结果并签订补充合同
9
万元,采购了
WPS
在线文档编辑服务。办公室领导对新的插件十分满意。

而在
敏捷开发部分
,我们拥抱变更,不再走变更流程,把用户的需求转化为一个个用户故事,加入产品待办事项列表里并进行优先级排序。在每两周的迭代计划会议上选取优先级最高的用户故事加入迭代事项列表中。

审计、检查

AA
成本:

一、规划成本管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了
如何进行成本管理工作。
项目规划阶段,
我带领项目团队以
项目管理计划

项目章程
为依据,结合公司知识库中的成本管理计划模板,进行了成本管理计划的编制,为日后项目顺利实施提供良好基础。这份计划确立了绩效测量规则、估算单位标准、成本报告的具体格式等内容。例如针对人力资源,我们规定使用人
/
天作为计量标准,
1

/
天为
2500
元。成本管理计划编写完成后,发起评审会议通过后把成本管理计划发送给各项目干系人。

二、估算成本

有了完善的成本管理计划,更应按计划贯彻执行
。资源需求、范围基准、项目管理计划
=

应急储备
、类比估算、标杆对照

自下而上估算
=>
成本估算、估算依据


敏捷开发
阶段,我们定义项目中最小功能为
1
点功能点,以此作为基点,算出其他的每项工作的功能点。

三、制定预算

我们对项目的成本进行了汇总,首先汇总各个活动的成本估算及其应急储备,得到相关工作包的成本;
然后汇总各个工作包的成本估算及其应急储备,得到控制帐户的成本;接着再汇总各个控制帐户的成本,得到成本基准;考虑到项目的各种风险和不确定等因素,我们为项目预留一部分管

理储备,最终形成了项目资金需求。

范围基准、成本估算、项目管理计划
=

成本汇总
、储备分析
=

成本基准、项目资金需求
(成本基准
+

管理储备

四、控制成本

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误

控制成本在整个项目生命周期中必不可少。具体而言,我要求项目成员每周提供项目周报,每周周五下午开展周例会对当前项目的进度情况评估和分析,形成
工作绩效信息
,并使项目一直行驶在规定的轨道上。

EV PV
分析原因、纠正措施、

变更举例

AA
沟通:

这两者之间既有区别又有联系,沟通管理主要是对信息的管理,而干系人管理主要是对人的管理。
通过沟通管理解决了沟通内容的问题,而通过干系人管理则解决了沟通对象问题。如果沟通管理不当,可能导致信息传递延误、向错误的干系人传递信息、与干系人沟通不足或误解
等问题。而干系人管理不到位,比如没有有效识别出关键干系人,或因干系人分类错误而缺乏及时有效的沟通时,可能因无法获得来自干系人的支持而导致项目失败。

本文中,我特别围绕干系人管理和沟通管理进行论述。

一、规划沟通管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何进行沟通管理工作。
项目规划阶段,我带领项目团队以
项目管理计划

干系人登记册
为依据,结合公司知识库中的沟通管理计划模板,进行了沟通管理计划的编制。首先我和项目团队召开会议对干系人登记册中的所有干系人进行沟通需求分析,使用
权利
/
利益矩阵

将所有干系人分成四类。第一类是公司内的信息分管院领导与信息部总监,对项目影响重大,而且非常关注项目结果,权高利高,应该
重点管理、即时报告
。第二类是综合办公室、财务部、审计部、人资部等职能部门的负责人,他们有较大权利,但是一般不会是系统的主要使用者,权高利低,应该
令其满意
。第三类是项目组成员以及重度使用系统的一线工作人员,权低利高,我们要
随时告知
项目情况。第四类是公司内的所有普通员工,权低利低,我们要持续
监督
,尽量提高其使用体验,降低他们对
OA
流程合规化的抵触心理。最终汇总上述信息并经过评审,编制出项目沟通管理计划,确定了在项目的各阶段,分别用何种工具、以何种方式对特定的干系人进行最有效的沟通工作。

二、管理沟通

有了完善的沟通管理计划,更应按计划贯彻执行

IPAD
面板、在线文档、企微拉群、输出

项目沟通记录

敏捷:迭代计划会、迭代评审会、迭代回顾会

三、监督沟通

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误,持续监督沟通
在整个项目生命周期中必不可少

具体而言,我要求项目成员每周提供项目周报,每周周五下午开展周例会对当前项目的进度进行评估和分析,
形成工作绩效信息
,并使项目一直行驶在规定的轨道上
。其次,在项目中也遇到了很多沟通的问题,比如固定资产标签打印问题,产品经理觉得需要对接标签打印机的底层,打印出预计的标签。而开发认为这个标签打印机过于老旧,对接困难,开发难度太大影响项目进度。我作为甲方的项目经理对需求又进行了深入了解,在网上多番调查的情况下,找到了此标签打印机的固定算法
128code
,使不需要对接标签打印机底层也能直接用算法生成标签,使问题得到了解决,节约了时间又降低了技术难度,双方都对结果满意,沟通达成一致。

变更请求
举例
~

A
范围:

一、规划范围管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了
如何进行成本管理工作。
我带领项目团队以
项目管理计划

项目章程
为依据,结合公司知识库中的范围管理计划模板,进行了范围管理计划的编制,为日后项目顺利实施提供良好基础。计划包括如何制定项目范围说明书,如何根据项目范围说明书创建
WBS
,如何维护和批准
WBS
,以及如何确认和正式验收己完成的可交付成果等内容。

二、收集需求

范围管理计划、招标文件、投标文件、合同
=
》标杆对照、工作跟随、思维导

图、原型法、
访谈
=

需求文件

需求跟踪矩阵

三、定义范围

项目章程、范围管理计划、需求文件
=
》产品分析


=

项目范围说明书
(业务方签字确认、评审,内容包括:
产品范围描述、可交付成果、验收标准、除外责任、制约因素、假设条件

四、创建
WBS

项目范围说明书、需求文件
=

分解
、用户故事、产品待办事项列表(
敏捷


=

WBS
(范围基准)。

五、确认范围

测试后的软件
=
》看板、检查、

迭代评审会

敏捷

=
》略

六、控制范围

对比基线、分析原因、偏差分析、纠正措施、
变更举例

A
干系人:

一、识别干系人

项目启动阶段,根据发布的项目章程和采购文件,通过会议和干系人分析的工具,将大多数的干系人识别出来,并使用
权利
/
利益方格

,将干系人进行分类,形成了一份
干系人登记册

这份登记册把干系人分成四类,第一类是公司内的信息分管院领导与信息部总监,对项目影响重大,而且非常关注项目结果。第二类是综合办公室、财务部、审计部、人资部等职能部门的负责人,他们有较大权利,但是一般不会是系统的主要使用者,对项目关注度一般。第三类是项目组成员以及重度使用系统的一线工作人员,尽管该类干系人权利低,但他们是软件的直接使用者。第四类是公司内的所有普通员工。

二、规划干系人参与

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何进行干系人管理工作。
我带领项目团队以
干系人登记册
为依据,对项目干系人的影响和利益进行分析,并进行优先级排序,再评估出干系人的所需参与程度和当前参与程度,最终形成了一份干系人管理计划,为日后项目顺利实施提供良好基础。

第一类的干系人权高利高,应该
重点管理、即时报告
。第二类的干系人权高利低,应该
令其满意
。第三类干系人权低利高,我们要
随时告知
项目情况,做好协调沟通。第四类是公司内的所有普通员工,我们要持续
监督
,尽量提高其使用体验,降低他们对
OA
流程合规化的抵触心理。

三、管理干系人参与

有了完善的管理计划,更应按计划贯彻执行。
管理干系人参与是在项目的整个生命周期中与干系人进行沟通和协作,以满足其需要与期望,解决实际出现的问题,并促进干系人合理参与项目活动的过程,有助于提升项目经理来自干系人的支持,并把干系人的抵制降到最低,从而显著提高项目成功的机会。

我们根据
干系人管理计划

干系人登记册
,积极与领导层、职能部门一线工作人员、项目团队成员进行交互式沟通。我们把主要干系人都拉入企业微信群组,并使用
IPAD
看板,持续公布团队做了什么,还需要做什么,让干系人实时知晓项目的情况,另外,每周召开项目评审会或敏捷的迭代评审会进行沟通协调,保证信息无障碍传递。种种措施,赢得了来自多方干系人的支持,大大减轻了项目实施的压力。

四、监督干系人参与

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误

持续监督干系人参与在整个项目生命周期中必不可少。
我深知,在整个项目生命周期中,干系人的参与对项目的成果至关重要。我使用
干系人参与评估矩阵
来记录干系人的当前参与程度,并按
不知晓、抵制、中立、支持、领导
等进行分类,积极沟通协调,使干系人达到计划的参与程度。在项目实施的过程中,曾出现开发越过项目经理擅自接收财务方提出的变更需求,结果导致借款审批流程和需求原型不符,我及时了解情况,召集财务、产品、开发及时面谈,形成共识,所有的需求变更都要走变更流程。在每个迭代结束,产品发布上线前,我还组织职能部门干系人参加我们的迭代评审会,以保证项目的成功上线。

B
进度:

规划进度管理:范围管理计划、项目章程
=
》会议评审
=

进度管理计划

定义活动:范围基准、项目管理计划、项目章程
=

滚动式规划
、分解(需求分析、蓝图设计、系统建设、系统测试、系统上线分解为具体的活动)
=

活动清单
、活动属性、
里程碑清单

排列活动顺序:活动清单、里程碑清单、范围基准
=
》前导图法绘制 (举个例,先做啥后做啥)
=

项目进度网络图

估算活动持续时间:活动清单、进度管理计划、范围基准
=
》类比估算、三点估算、自下而上估算(加上
10%
的应急储备时间)
=

持续时间估算、估算依据

制定进度计划:范围基准、持续时间估算、估算依据、项目进度网络图
=

敏捷发布规划
、关键路径法、资源平衡
=

项目进度计划

进度基准

控制进度:纠偏、变更、偏差分析、
举例说明进度问题
(赶工、快速跟进)。

B
质量:

规划质量管理:
)如何进行评审流程、测试流程

质量测量指标、质量管理计划
(用户满意度不小于
...
任何操作
2ms

...
系统故障率
...

管理质量:
举例说明测试的问题
(
没有自测等
)
,改进质量、形成质量意识
/
文化、根本原因分析、审计
=
》 指令报告

控制质量:核对单、检查、根本原因分析、测试(单元、集成、系统、验收测试)
=
》 合适的可交付成果

C
风险:

一、规划风险管理

所谓磨刀不误砍柴功,在正式工作之前,我们首先考虑了如何进行风险管理工作。
我带领项目团队以
项目管理计划

项目章程
为依据,联合项目主要干系人,
编制了风险管理计划,
为项目的顺利实施奠定了良好的基础
。该计划
包括
风险管理策略、角色和职责、管理应急储备、概率和影响矩阵、干系人风险偏好、风险类别、风险概率
和影响定义
等内容。
在计划中,我们还确定了每两周召开一次风险评估会议,评估项目活动的最新风险状态。

二、识别风险

风险识别是判断哪些风险会对项目产生影响并记录过程。
根据项目的实际情况

我们将风险划分为
技术风险、管理风险和外部风险
三大类,并使用
风险分解结构(
RBS)

列出已知的风险,然后进一步细化这些已识别的风险,形成包含
风险描述、潜在责任人和潜在应对措施

风险登记册
。在
敏捷开发
阶段,风险识别是一个持续的过程,我们通过
10分钟的

每日站会
分享和记录可能的风险,并在每个迭代周期的迭代回顾会议上进行评审。

三、实施定性风险分析

定性风险分析是分析和评估风险的概率和影响,并对风险进行优先级排序的过程。我们组织公司财务、行政、人资等部门通过会议对已识别的风险进行
概率和影响评估,
我们采用头脑风暴的方法来确定每个风险发生的可能性以及对项目的影响,最后再进行进行风险优先级的排序

在这一过程中,我们为风险登记册添加了
责任人、概率、影响、分值、风险类别和策略
等新的信息。

四、实施定量风险分析

定量风险分析是对风险进行量化计算和分析
并将结果汇总更新到风险登记册中
的过程
。但是,因为此次项目本身架构清晰,而项目组成员又对类似项目经验丰富,前期识别到的风险都完全处在可控范围内,所以我们根据项目管理的裁剪原则,
研究评审后决定
跳过实施定量风险分析过程。

五、规划风险应对

规划风险应对就是针对正面风险和负面风险,制定不同应对措施,以提高项目成功的机会或降低项目失败的威胁。我们以
风险管理计划

最新的风险登记册
为依据,分析各个风险应如何应对。在公司启动
IPO
的一年里,经常会被要求提交各种旧系统里的流程审批数据。

然而项目前期既无法确认哪些数据未来是会记事务所要提交的,也无法确定哪些数据无法导入新系统(新旧系统数据结构不同,早期无法确定是否能导入)。

为了解决这个问题,我们讨论后决定,未来能导入新系统的数据尽量导入,万一遇到无法导入的旧数据
,先用文件存档,再以图片的形式保存审批流程截图。通过以上措施,我们能够在系统切换到新系统后尽量保留原有的数据,以确保公司业务的顺利进行。

六、实施风险应对

有了完善的管理计划,更应按计划贯彻执行。
实施风险应对是根据制定的风险应对计划,执行
.....

举例(新冠)

七、监督风险

冰冻三尺非一日之寒,项目失败的背后是日积月累的错误,持续监督风险在整个项目生命周期中必不可少。

具体而言,我以
1-2周为节点,在风险评审会或敏捷的迭代回顾会议中与团队成员一起

评估风险是否变化,是否有新的风险出现,并且对已出现的风险评估应对措施执行的有效性。另外,还有公司审计部的定期实行
风险审计
,形成审计底稿供负责我院
IPO
的事务所审阅。

附录(敏捷流程及PMP知识点):