业务开发常见问题剖析

一、代码

1.判等问题

问题场景:

在实际业务开发过程中,比较值,对象或类型判断是非常常见的,可是有时明显数值相同的情况,却判断为不等,导致后续业务错误(对账业务,枚举类型判断)

可能原因:

==和equal的错误使用;
判断对象是否是同一个,没有重写hashcode()和equal()方法;
错误将包装类型和基本类型比较,或者更低级错误就是不同类型进行比较int和String

原因分析:

  1. equals 和 == 的区别

    • 对基本类型,比如 int、long,进行判等,只能使用 ==,比较的是直接值。因为基本类型的值就是其数值。
    • 对引用类型,比如 Integer、Long 和 String,进行判等,需要使用 equals 进行内容判等。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。

    在一些情况Integer和String也可以直接用==判断(java数值缓存[-128,127]和字符串驻留)

    Integer a = 127; //Integer.valueOf(127)
    Integer b = 127; //Integer.valueOf(127)
    log.info("\nInteger a = 127;\n" +
            "Integer b = 127;\n" +
            "a == b ? {}",a == b);    // true
    
    Integer c = 128; //Integer.valueOf(128)
    Integer d = 128; //Integer.valueOf(128)
    log.info("\nInteger c = 128;\n" +
            "Integer d = 128;\n" +
            "c == d ? {}", c == d);   //false
    
    Integer e = 127; //Integer.valueOf(127)
    Integer f = new Integer(127); //new instance
    log.info("\nInteger e = 127;\n" +
            "Integer f = new Integer(127);\n" +
            "e == f ? {}", e == f);   //false
    
    Integer g = new Integer(127); //new instance
    Integer h = new Integer(127); //new instance
    log.info("\nInteger g = new Integer(127);\n" +
            "Integer h = new Integer(127);\n" +
            "g == h ? {}", g == h);  //false
    
    Integer i = 128; //unbox(java会自动拆箱)
    int j = 128;
    log.info("\nInteger i = 128;\n" +
            "int j = 128;\n" +
            "i == j ? {}", i == j); //true
    String a = "1";
    String b = "1";
    log.info("\nString a = \"1\";\n" +
            "String b = \"1\";\n" +
            "a == b ? {}", a == b); //true
    
    String c = new String("2");
    String d = new String("2");
    log.info("\nString c = new String(\"2\");\n" +
            "String d = new String(\"2\");" +
            "c == d ? {}", c == d); //false
    
    #使用 String 提供的 intern 方法也会走常量池机制
    String e = new String("3").intern();
    String f = new String("3").intern();
    log.info("\nString e = new String(\"3\").intern();\n" +
            "String f = new String(\"3\").intern();\n" +
            "e == f ? {}", e == f); //true
    
    String g = new String("4");
    String h = new String("4");
    log.info("\nString g = new String(\"4\");\n" +
            "String h = new String(\"4\");\n" +
            "g == h ? {}", g.equals(h)); //true

    ==注意:特别在开发中定义枚举类型时,判断状态或类型要注意= =和equeal的使用(特别是Integer和int)==

  2. 在业务中有时也需要比较对象是否相同,如果不重写这两个方法则在比较时默认是使用object的equal方法,而object中equal方法比较的对象引用地址,所以这需要我们重写equeal和hashcode方法

    class PointRight {
        private final int x;
        private final int y;
        private final String desc;
      
       @Override
       public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        //思考instanceof和getClass有什么区别
        PointRight that = (PointRight) o;
        return x == that.x && y == that.y;
    }    
        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }
    PointWrong p1 = new PointWrong(1, 2, "a");
    PointWrong p2 = new PointWrong(1, 2, "b");
    
    HashSet points = new HashSet<>();
    points.add(p1);
    log.info("points.contains(p2) ? {}", points.contains(p2));//如果没有实现hashcode这边返回false,出现这个 Bug 的原因是,散列表需要使用 hashCode 来定位元素放到哪个桶。如果自定义对象没有实现自定义的 hashCode 方法,就会使用 Object 超类的默认实现,得到的两个 hashCode 是不同的,导致无法满足需求。

    而在我们实际开发中==Lombok==其实都帮我们实现好了这个方法@Data默认就重写了equeal和hashcode方法如果有继承关系需要用到父类属性则用==@EqualsAndHashCode(callSuper = true)==在属性上用[email protected]==可以排查相关属性

    instanceof进行类型检查规则是:你是该类或者是该类的子类; getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断。不会存在继承方面的考虑

2.空值问题

问题描述:

‘尊敬的 null 你好,XXX’ 只要做过开发肯定知道这其中有什么问题,毕竟是个程序员都逃不出这个问题,这其中包括java后台的NullPointerException,和null值处理问题,SQL中查询也需要注意

原因分析:

1.空指针异常

  • 参数值是Integer等包装类型,使用时因为自动拆箱出现了空指针异常;
  • 字符串比较出现空指针异常
  • 类似ConcurrentHashMap的容器不支持key和value为null,如果设置空会报空指针异常
  • A对象包含了B对象,在通过A对象的字段获得B之后,没有对字段判空,就级联调用B的方法出现空指针异常
  • 方法或远程服务返回的List不是空列表而是Null,没有进行判空直接调用List的方法或遍历出现空指针异常

    private List wrongMethod(FooService fooService, Integer i, String s, String t) {
        log.info("result {} {} {} {}", i + 1, s.equals("OK"), s.equals(t),
                new ConcurrentHashMap().put(null, null));
        if (fooService.getBarService().bar().equals("OK"))
            log.info("OK");
        return null;
    }
    
    @GetMapping("wrong")
    public int wrong(@RequestParam(value = "test", defaultValue = "1111") String test) {
        return wrongMethod(test.charAt(0) == '1' ? null : new FooService(),
                test.charAt(1) == '1' ? null : 1,
                test.charAt(2) == '1' ? null : "OK",
                test.charAt(3) == '1' ? null : "OK").size();
    }
    
    class FooService {
        @Getter
        private BarService barService;
    
    }
    
    class BarService {
        String bar() {
            return "OK";
        }
    }
    //对入参 Integer i 进行 +1 操作;
    //对入参 String s 进行比较操作,判断内容是否等于"OK";
    //对入参 String s 和入参 String t 进行比较操作,判断两者是否相等;
    //对 new 出来的 ConcurrentHashMap 进行 put 操作,Key 和 Value 都设置为 null。
  1. mysql中null值问题

    //比如在user表中有个score字段可以为null
    SELECT SUM(score) FROM user
    SELECT COUNT(score) FROM user
    SELECT * FROM user WHERE score=null
  • 通过 sum 函数统计一个只有 NULL 值的列的总和,比如 SUM(score),结果都是NULL 期望为0
  • select 记录数量,count 使用一个允许 NULL 的字段,比如 COUNT(score), 结果也是NULL 期望为1
  • 使用 =NULL 条件查询字段值为 NULL 的记录,比如 score=null 条件。结果是查询不到 期望查到对应行

解决方案:

  1. 空指针异常
  • 对于 Integer 的判空,可以使用 Optional.ofNullable 来构造一个 Optional,然后使用 orElse(0) 把 null 替换为默认值再进行 +1 操作。
  • 对于 String 和字面量的比较,可以把字面量放在前面,比如"OK".equals(s),这样即使 s 是 null 也不会出现空指针异常;而对于两个可能为 null 的字符串变量的 equals 比较,可以使用 Objects.equals,它会做判空处理。
  • 对于 ConcurrentHashMap,既然其 Key 和 Value 都不支持 null,修复方式就是不要把 null 存进去。HashMap 的 Key 和 Value 可以存入 null,而 ConcurrentHashMap 看似是 HashMap 的线程安全版本,却不支持 null 值的 Key 和 Value,这是容易产生误区的一个地方。
  • 对于类似 fooService.getBarService().bar().equals(“OK”) 的级联调用,需要判空的地方有很多,包括 fooService、getBarService() 方法的返回值,以及 bar 方法返回的字符串。如果使用 if-else 来判空的话可能需要好几行代码,但使用 Optional 的话一行代码就够了。
  • 对于 rightMethod 返回的 List,由于不能确认其是否为 null,所以在调用 size 方法获得列表大小之前,同样可以使用 Optional.ofNullable 包装一下返回值,然后通过.orElse(Collections.emptyList()) 实现在 List 为 null 的时候获得一个空的 List,最后再调用 size 方法。
  1. mysql中空值问题
  • MySQL 中 sum 函数没统计到任何记录时,会返回 null 而不是 0,可以使用 IFNULL 函数把 null 转换为 0;
  • MySQL 中 count 字段不统计 null 值,COUNT(*) 才是统计所有记录数量的正确方式。
  • MySQL 中使用诸如 =、<、> 这样的算数比较操作符比较 NULL 的结果总是 NULL,这种比较就显得没有任何意义,需要使用 IS NULL、IS NOT NULL 或 ISNULL() 函数来比较。

    SELECT IFNULL(SUM(score),0) FROM `user`
    SELECT COUNT(*) FROM `user`
    SELECT * FROM `user` WHERE score IS NULL

3.数据库查询问题

问题场景: 刷新某个界面和,查询某个接口时,总是一段时间才出来结果,这其中也许就是慢查询的问题。

可能原因:

无sql索引  (思考:添加索引就可以加快查询速度吗?)
limit深分页
单表数据量太大
join或者子查询过多
in元素过多
数据库存在刷脏页
拿不到锁
delete+in子查询不走索引
group by使用临时表和文件排序
系统或网络资源不够

排查原因

开启sql慢查询日志

  1. 临时生效

    SQL 语句 描述 说明
    SHOW VARIABLES LIKE '%slow_query_log%'; 查看慢查询日志是否开启 默认情况下 slow_query_log 的值为 OFF, 表示慢查询日志是禁用的
    set global slow_query_log=1; 开启慢查询日志 只对当前数据库生效,如果 mysql 重启,则会失效。
    SHOW VARIABLES LIKE 'long_query_time%'; 查看慢查询设定阈值 单位秒,默认是10秒
    set global long_query_time=3; 设定慢查询阈值 单位秒,大于3秒,而不是大于等于
  2. 永久生效

    需要更改配置文件my.cnf中[mysqlId]下的配置

    [mysqld] 
    slow_query_log=1 
    slow_query_log_file=/var/lib/mysql/slow.log 
    long_query_time=3 
    log_output=FILE
  3. 运行查询时间长的sql,打开慢查询日志查看

工作常用分析命令

# 得到返回记录集最多的 10 个 SQL 
mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log 
# 得到访问次数最多的 10 个 SQL 
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log 
# 得到按照时间排序的前 10 条里面含有左连接的查询语句 
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow.log 
另外建议在使用这些命令时结合 | 和 more 使用 ,否则有可能出现爆屏情况 
mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log | more

测试

# 查看慢查询日志是否开启,默认情况下关闭(这边已经开启了)
mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-------------------------+
| Variable_name       | Value                   |
+---------------------+-------------------------+
| slow_query_log      | ON                      |
| slow_query_log_file | /var/lib/mysql/slow.log |
+---------------------+-------------------------+
2 rows in set (0.01 sec)
# 查看慢查询设定阈值,这边设置3s
mysql> SHOW VARIABLES LIKE 'long_query_time%';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| long_query_time | 3.000000 |
+-----------------+----------+
1 row in set (0.00 sec
# 测试慢查询,等待4秒
mysql> select sleep(4);
+----------+
| sleep(4) |
+----------+
|        0 |
+----------+
1 row in set (4.00 sec)

----------------------------------
#测试结果,可以看到测试生效
root@9c5c5d1940e9:/# cat /var/lib/mysql/slow.log 
mysqld, Version: 5.7.34-log (MySQL Community Server (GPL)). started with:
Tcp port: 0  Unix socket: (null)
Time                 Id Command    Argument
# Time: 2022-07-18T06:44:15.162747Z
# User@Host: root[root] @ localhost []  Id:     2
# Query_time: 4.000267  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 0
SET timestamp=1658126655;
select sleep(4);

思考:

添加索引就可以加快查询速度吗?

索引失效场景及优化:

  • 隐式的类型转换、索引失效

    order_id为字符串类型可是查询用数值类型则导致失效

    业务开发常见问题剖析_第1张图片

  • 查询条件包含or,可能导致失效

    business_id 不为索引,导致索引失效

    业务开发常见问题剖析_第2张图片

  • like通配符可能导致索引失效

    使用%在左侧导致索引失效

    业务开发常见问题剖析_第3张图片

  • 查询条件不满足联合索引的最左配置原则

    联合索引(a,b,c)在查询条件可以是 a ,(a,b),(a,b,c) 这些都是走索引,但是单独查 b, c, (a,c) (b,c) 不走索引

  • 业务开发常见问题剖析_第4张图片

    image-20220809101515366

    业务开发常见问题剖析_第5张图片

  • 在索引列上使用mysql内置函数

    order_time为索引,但是使用内置函数就失效了,可将内置函数逻辑写到右边

    image-20220809102622777

    image-20220809102741731

  • 对索引进行运算(+ - * /) 索引失效

    业务开发常见问题剖析_第6张图片

  • 索引字段上使用(!= 或者<>),索引失效
  • 业务开发常见问题剖析_第7张图片
  • 索引字段上使用is null,is not null,索引可能失效

    连个都为索引但是用or连接索引失效

    业务开发常见问题剖析_第8张图片

  • 左右连接,关联的字段编码格式不一样
  • 优化器选错了索引

4.时间问题

问题描述:用docker部署的后端项目和数据库,时间接口返回时间与实际差8小时

可能原因:

1. 容器的时区与实际时区相差8小时
2. jvm时区与实际时区相差8小时
3. 存入数据库后时间相差8小时
4. 后端获取时间一致,但是返回前端时间相差8小时

排查原因:排查顺序

  1. 进入服务和数据库容器查看容器时区(CST应该是指(China Shanghai Time,东八区时间)UTC应该是指(Coordinated Universal Time,标准时间))

    root@bba4691ecdc3:/# date
    Fri Aug  5 01:58:31 UTC 2022
    Fri Aug  5 09:59:19 CST 2022
  2. 进入java程序编写测试方法,new date()调用的是jvm时间

    import java.util.Date;
    
    public class Demo {
       public static void main(String[] args) {
           Date date = new Date();
           System.out.println(date);
       }
    }
  3. 打开数据库,查看数据库存储的时间是否和实际要存的实际一致
  4. 在后台打印从数据库获取的时间,和前端显示的时间是否一致

解决方案:

  1. 在构建容器时需要事先设置好容器时间环境,DockerFile中可以添加

    RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
    RUN echo 'Asia/Shanghai' >/etc/timezone

    在docker-compose.yml文件中可以添加设置

    environment:
      - TZ=Asia/Shanghai
  2. jvm时区与实际时区相差8小时,这可以全局配置

    @PostConstruct
    void started() {
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
    }
  3. 存入数据库时间与实际差8小时,就是在spring配置连接数据库时没有设置时区,可以在url后加

    serverTimezone=GMT%2B
  4. 返回前端时间不一致,springboot中对加了@RestController或者@[email protected]注解的方法的返回值默认是Json格式, 所以,对date类型的数据,在返回浏览器端时,会被springboot默认的Jackson框架转换,而Jackson框架默认的时区GMT(相对于中国是少了8小时)。所以最终返回到前端结果是相差8小时。这个可以全局配置

    spring:
      jackson:
        time-zone: GMT+8
        date-format: yyyy-MM-dd HH:mm:ss

思考:

数据库中datetime和timestamp有什么区别?

目前项目都是供国内用户使用,如果项目用户在国外呢,这个该如何处理?

4.HTTP调用、超时、重试、并发

二、设计

1.代码重复

问题场景:

​ 在开发中有许多场景是根据不同判断逻辑执行不同的操作,比如用户支付时选择不同的支付渠道,每种支付渠道对应的实现逻辑不同,又或者在下单时,不同用户角色或者下单金额选择不同的优惠方式,而在这个过程中,按传统实现不可避免有大量重复代码和if else判断,也许这样是简单,但是对代码后续扩展有很大的难度。

案例分析:

假设 现在有一个需求购买商品的需求

  • 普通用户收取运费,运费为商品的10%,无商品折扣
  • VIP用户也收取10%手续费,可是再购买两件相同商品时,第二件半价
  • 内部用户不收手续费,但是没有商品折扣

针对这个需求分析,如果直接开始写是不是这样定义三个类分别实现对应的逻辑

//购物车
@Data
public class Cart {
    //商品清单
    private List items = new ArrayList<>();
    //总优惠
    private BigDecimal totalDiscount;
    //商品总价
    private BigDecimal totalItemPrice;
    //总运费
    private BigDecimal totalDeliveryPrice;
    //应付总价
    private BigDecimal payPrice;
}
//购物车中的商品
@Data
public class Item {
    //商品ID
    private long id;
    //商品数量
    private int quantity;
    //商品单价
    private BigDecimal price;
    //商品优惠
    private BigDecimal couponPrice;
    //商品运费
    private BigDecimal deliveryPrice;
}
//普通用户购物车处理
public class NormalUserCart {
    public Cart process(long userId, Map items) {
        Cart cart = new Cart();
        //把Map的购物车转换为Item列表
        List itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        //处理运费和商品优惠
        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });
        //计算商品总价
     cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算运费总价
  cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总优惠
  cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //应付总价=商品总价+运费总价-总优惠
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }
}

然后实现针对 VIP 用户的购物车逻辑。与普通用户购物车逻辑的不同在于,VIP 用户能享受同类商品多买的折扣。所以,这部分代码只需要额外处理多买折扣部分:

public class VipUserCart {


    public Cart process(long userId, Map items) {
        ...


        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            //购买两件以上相同商品,第三件开始享受一定折扣
            if (item.getQuantity() > 2) {
                item.setCouponPrice(item.getPrice()
                        .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                       .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
            } else {
                item.setCouponPrice(BigDecimal.ZERO);
            }
        });


        ...
        return cart;
    }
}

最后是免运费、无折扣的内部用户,同样只是处理商品折扣和运费时的逻辑差异:

public class InternalUserCart {


    public Cart process(long userId, Map items) {
        ...

        itemList.stream().forEach(item -> {
            //免运费
            item.setDeliveryPrice(BigDecimal.ZERO);
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        ...
        return cart;
    }
}

从这里就可以看到有大量的重复逻辑,如果这时需要改里面一处,就要同时改3个地方代码。特别是加需求或则改bug的时候,这个实在太痛了。

有了三个购物车处理逻辑,我们就需要根据不同的用户类型使用不同的购物车了。如下代码所示,使用三个 if 实现不同类型用户调用不同购物车的 process 方法:

@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
    //根据用户ID获得用户类型
    String userCategory = Db.getUserCategory(userId);
    //普通用户处理逻辑
    if (userCategory.equals("Normal")) {
        NormalUserCart normalUserCart = new NormalUserCart();
        return normalUserCart.process(userId, items);
    }
    //VIP用户处理逻辑
    if (userCategory.equals("Vip")) {
        VipUserCart vipUserCart = new VipUserCart();
        return vipUserCart.process(userId, items);
    }
    //内部用户处理逻辑
    if (userCategory.equals("Internal")) {
        InternalUserCart internalUserCart = new InternalUserCart();
        return internalUserCart.process(userId, items);
    }

    return null;
}

这里可能后续又会有更多的用户类型,又要多加if判断,又要对代码进行修改学过设计模式都知道这边违反了开闭原则。

从上面的例子可以看出来有两个问题,大量重复代码和大量if判断,接下来可以用两种设计模式来优化

优化方案:

模板方法模式:在父类定义一个操作中的算法骨架,而将算法的一些因具体情况而定的步骤延迟到子类中实现,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

业务开发常见问题剖析_第9张图片

public abstract class AbstractCart {
    //处理购物车的大量重复逻辑在父类实现
    public Cart process(long userId, Map items) {

        Cart cart = new Cart();

        List itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        //让子类处理每一个商品的优惠
        itemList.stream().forEach(item -> {
            processCouponPrice(userId, item);
            processDeliveryPrice(userId, item);
        });
        //计算商品总价
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总运费
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算应付价格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }

    //处理商品优惠的逻辑留给子类实现
    protected abstract void processCouponPrice(long userId, Item item);
    //处理配送费的逻辑留给子类实现
    protected abstract void processDeliveryPrice(long userId, Item item);
}
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(item.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()))
                .multiply(new BigDecimal("0.1")));
    }
}
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        if (item.getQuantity() > 2) {
            item.setCouponPrice(item.getPrice()
                    .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                    .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
        } else {
            item.setCouponPrice(BigDecimal.ZERO);
        }
    }
}
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {
    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(BigDecimal.ZERO);
    }
}

工厂模式:

这边利用spring容器实现

@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
    String userCategory = Db.getUserCategory(userId);
    AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
    return cart.process(userId, items);
}

2.接口设计

3.缓存设计

三、安全

1.资金操作

问题描述:

​ 在用户支付下单这个操作,其实牵扯到非常多的系统,支付链路中一个环节出现问题,就可能导致业务逻辑的奔溃,这边我我是从系统层面上来说支付可能存在的问题

掉单异常

在一帮支付流程中至少会有涉及3个系统外部商户、第三方支付公司、银行存在订单状态不一致

业务开发常见问题剖析_第10张图片

大致流程为

  1. 携程创建订单,向第三方支付公司发起支付请求
  2. 第三方支付公司创建订单,并向工行发起支付请求
  3. 工行完成扣款操作,返回第三方支付公司
  4. 第三方支付完成订单更新并返回携程
  5. 携程变更订单状态

业务开发常见问题剖析_第11张图片

在这个过程中,可能会碰到用户银行已经扣款了,可是携程订单还是待支付,这就是掉单

而多数调单情况是3,5两步骤,这种是由于外部因素所以称==外部掉单==,少部分是因为4,6即系统内部更新订单状态失败导致的,这就是==内部掉单==

  • 针对外部掉单的补救方法

    1. 最简单的方法【适当增加超时时间】这里需要注意增加了超时时间,可能整个链路时间会被拉长导致系统出现问题的几率加大(对接外部系统需要设置超时时间和读取超时间)
    2. 接收异步回调

      业务开发常见问题剖析_第12张图片

      大部分情况都是有提供异步回调接口的,我们在异步回调中更改订单状态,不过需要注意两点

      1. 对于异步请求信息,一定需要对通知内容进行签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失
      2. 异步通知将会发送多次,所以异步通知处理需要幂等。
  1. 掉单查询,我们可以将这类超时未知的订单单独存放到掉单表中,然后定时去渠道侧查询订单状态

    业务开发常见问题剖析_第13张图片

    1. 最后兜底对账,将渠道侧提供的对账文件和系统的对账文件做比对
  • 针对内部调单的补救
  1. 用分布式事务
  2. 内部对账

重复支付

订单失效异常

  1. 在下订单时,用户长时间停留在支付页面但是不支付,这时订单在关闭最后时间用户支付了,这时订单显示支付超时已关闭,这时用户已经收到扣款成功的消息,
  2. 用户在有效期下单,可是因为网络问题导致,商户没有收到支付成功结果,导致订单被取消。

解决方法:

1. 上送订单有效期到支付渠道
  1. 内部退款

2.如何保证数据和传输数据

  1. 保存敏感数据
  2. 传输敏感数据

你可能感兴趣的:(业务开发)