乐趣区

关于业务:业务开发常见问题剖析

一、代码

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<PointWrong> 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)== 在属性上用 ==@EqualsAndHashCode.Exclude== 能够排查相干属性

    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<String> wrongMethod(FooService fooService, Integer i, String s, String t) {log.info("result {} {} {} {}", i + 1, s.equals("OK"), s.equals(t),
                new ConcurrentHashMap<String, String>().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 为字符串类型可是查问用数值类型则导致生效

  • 查问条件蕴含 or, 可能导致生效

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

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

    应用 % 在左侧导致索引生效

  • 查问条件不满足联结索引的最左配置准则

    联结索引 (a,b,c) 在查问条件能够是 a ,(a,b),(a,b,c) 这些都是走索引,然而独自查 b, c, (a,c) (b,c) 不走索引

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

    order_time 为索引,然而应用内置函数就生效了,可将内置函数逻辑写到左边

  • 对索引进行运算(+ – * /) 索引生效

  • 索引字段上应用(!= 或者 <>), 索引生效
  • 索引字段上应用 is null,is not null, 索引可能生效

    连个都为索引然而用 or 连贯索引生效

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

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<Item> 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<Long, Integer> items) {Cart cart = new Cart();
        // 把 Map 的购物车转换为 Item 列表
        List<Item> 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<Long, Integer> 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<Long, Integer> 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 判断,接下来能够用两种设计模式来优化

优化计划:

模板办法模式:在父类定义一个操作中的算法骨架,而将算法的一些因具体情况而定的步骤提早到子类中实现,使得子类能够不扭转该算法构造的状况下重定义该算法的某些特定步骤。

public abstract class AbstractCart {
    // 解决购物车的大量反复逻辑在父类实现
    public Cart process(long userId, Map<Long, Integer> items) {Cart cart = new Cart();

        List<Item> 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 个零碎内部商户、第三方领取公司、银行存在订单状态不统一

大抵流程为

  1. 携程创立订单,向第三方领取公司发动领取申请
  2. 第三方领取公司创立订单,并向工行发动领取申请
  3. 工行实现扣款操作,返回第三方领取公司
  4. 第三方领取实现订单更新并返回携程
  5. 携程变更订单状态

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

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

  • 针对内部掉单的补救办法

    1. 最简略的办法【适当减少超时工夫】这里须要留神减少了超时工夫,可能整个链路工夫会被拉长导致系统呈现问题的几率加大(对接内部零碎须要设置超时工夫和读取超工夫)
    2. 接管异步回调

      大部分状况都是有提供异步回调接口的,咱们在异步回调中更改订单状态,不过须要留神两点

      1. 对于异步申请信息,肯定须要对告诉内容进行签名验证,并校验返回的订单金额是否与商户侧的订单金额统一,避免数据透露导致呈现“假告诉”,造成资金损失
      2. 异步告诉将会发送屡次,所以异步告诉解决须要幂等。
  1. 掉单查问,咱们能够将这类超时未知的订单独自寄存到掉单表中,而后定时去渠道侧查问订单状态

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

反复领取

订单生效异样

  1. 在下订单时,用户长时间停留在领取页面然而不领取,这时订单在敞开最初工夫用户领取了,这时订单显示领取超时已敞开,这时用户曾经收到扣款胜利的音讯,
  2. 用户在有效期下单,可是因为网络问题导致,商户没有收到领取胜利后果,导致订单被勾销。

解决办法:

1. 上送订单有效期到领取渠道
  1. 外部退款

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

  1. 保留敏感数据
  2. 传输敏感数据
退出移动版