Blage's Coding Blage's Coding
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
  • 网关

  • 抽奖

  • 电商

    • 微服务组件快速使用
    • 电商
      • 1.生成菜单树
      • 2.菜单删除
        • 逻辑删除
      • 3.文件存储OSS
        • 基本概念
        • SDK
      • 4.JSR303后端数据校验
        • 基本使用
        • 分组校验
        • 自定义校验
      • 5.统一异常处理
      • 6.Json空字段转换和日期
        • 对象字段为空转换Json
        • 设置统一日期格式
      • 7.后端开发对象类型
      • 8.压测
        • 优化策略
        • 压测报错
      • 9.缓存
        • Redisson-分布式锁
        • SpringCache
      • 10.异步
        • 线程池
        • CompletableFuture
      • 11.认证服务OAuth2
        • 社交登录
        • 单点登录
        • 认证授权
      • 12.提交订单接口幂等性
      • 13.Aop解决本地事务失效
      • 14.分布式事务
        • Raft算法
        • seata
      • 15.延时队列
      • 16.消息可靠性
      • 17.第三方应用支付-加密算法
        • 对称/非对称加密
        • 内网穿透
        • 异步通知
      • 18.秒杀服务
        • 秒杀预热
        • 秒杀链接加密
        • 异步处理订单信息
      • 19.部署配置
  • 外卖

  • 项目笔记
  • 电商
phan
2023-05-15
目录

电商

# 电商

# 1.生成菜单树

使用list集合的stream().filter(),map(),sorted()方法来实现。

  • filter():筛选过滤集合中满足的item,并return返回一个布尔类型,如果为true则表示item留在集合中。
  • map():对所有的item进行操作,返回对象类型可以是item类型,也可以是自定义new的新类型,并把这些return的对象收集起来。
  • sorted:升序排序,return顺序和参数顺序一致则升序,否则降序。
@Override
public List<CategoryEntity> getListWithTree() {
    List<CategoryEntity> list = this.list();
    List<CategoryEntity> collect = list.stream().filter(entity -> entity.getParentCid() == 0)
            .map(entity -> {
                entity.setChildren(getChildList(entity, list));
                return entity;
            }).sorted((entity1, entity2) -> {
                return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
            }).collect(Collectors.toList());
    return collect;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.菜单删除

# 逻辑删除

实际后端调用deleteById时,数据库后端并不删除该行记录,仅仅使用表中的show_status位来表示是否删除

  • 配置yml中MP的逻辑删除字段值,1则不删除,0则删除。
mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 0
      logic-not-delete-value: 1
1
2
3
4
5
  • 在实体类上添加@TableLogic标注逻辑删除字段

# 3.文件存储OSS

# 基本概念

Bucket:一个存储空间,一般一个项目对应一个Bucket

Object:一个文件内容称之为一个对象

endpoint:地域名称

权限:①公共读:所有人都可以读②私有:只有通过账号的access账号密码才能读写③公共读写:任何人都可以读写

过程:前端组件在upload图片之前,先向后端服务器发送一个axios请求,要一个根据阿里云账号密码生成的防伪签名policy—>前端组件带着签名和图片数组直接上传到action的oss地址。

image-20230318203416107

注意:①需要给accessKey子账户设置oss访问权限②需要给OSS的Bucket设置跨域(这里前端组件拿到签名和图片后,发送请求是前端—>阿里云oss,不过后端,因此需要解决跨域)

# SDK

  • pom引入依赖
<dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
1
2
3
4
  • yml中(或者Nacos配置中心)配置access的key和密码,endpoint,bucket
spring:
  cloud:
    alicloud:
      access-key: LTAI5t7FtdaodMbfrCJwFQEc
      secret-key: aP5TIVWb31TVFlsA4CTNOgciHLvZJz
      oss:
        endpoint: oss-cn-beijing.aliyuncs.com
        bucket: mybucket
1
2
3
4
5
6
7
8
  • @Autowired自动注入OSSClient对象,使用封装的ossClient进行操作。其中@Value从配置中心获取数据。
@RestController
public class OssController {
    @Autowired
    OSS ossClient;
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;
    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;
    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;
    @RequestMapping("/oss/policy")
    public R policy() {
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String dir = format + "/"; // 用户上传文件时指定的前缀。
        Map<String, String> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return R.ok().put("data",respMap);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4.JSR303后端数据校验

# 基本使用

①在entity实体类属性上添加注解:

  • @NotBlank(message=""):字段不能为null且必须至少包含一个非空格字符,通过指定message来输出错误信息。
  • @URL:必须是一个URL地址
  • @Pattern(regexp=""):必须是正则表达式
  • @Min(value=0):必须小于value值

②在controller方法接收的前端形参实体添加@Valid校验注解,BindingResult封装校验结果。

@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
    if (result.hasErrors()) {
        HashMap<Object, Object> map = new HashMap<>();
        //获取校验错误结果
        result.getFieldErrors().forEach((item)->{
        //获取错误提示
        String message = item.getDefaultMessage();
        //获取错误的属性字段名
        String field = item.getField();
        map.put(field, message);
        });
        return R.error(400, "提交数据不合法").put("data", map);
    } else {
        brandService.save(brand);
    }
    return R.ok();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 分组校验

场景:多个不同业务场景下,每个属性字段需要校验的规则不同。

  • 给校验注解标注什么场景下需要进行校验@NotNull(groups={UpdateGroup.class})
  • 在controller方法接收的前端形参添加spring的校验注解@Validated({Addgroup.class}),实际上组就是用一个接口在Bean属性字段和Controller之间标注业务场景,不需要填写实际逻辑。

类似于策略模式,但是区别在于这里不需要实现接口,直接以不同接口区分不同业务场景;而策略模式是通过定义一个接口,然后不同业务实体类根据业务特点去实现接口方法。方法调用形参是接口,传入实参则是不同的实现类

# 自定义校验

  • 导入依赖artifactId=validation-api

  • 使用@interface编写一个自定义的校验注解类

    @Target:表示能在哪个位置进行注解,方法、属性...

    @Retention:运行时获取

    @Constraint:指定使用校验器

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {}
1
2
3
4
5
  • 编写自定义的校验器实现类(实现ConstraintValidator接口,指明校验数据基本类型),并关联到校验注解类。
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set = new HashSet<>();
    @Override     //初始化方法
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }
    @Override //判断校验是否成功,value:需要校验的值,context:上下文获取值
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5.统一异常处理

ControllerAdvice本质是一个Component,容器加载也会被spring扫描到。它会对需要扫描的controller对象添加规则,类似于AOP思想的一种实现,但并不是使用AOP来织入业务逻辑,而是Spring内置对其各个逻辑的织入方式进行了内置支持。

  • 创建一个类,标注@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")=@ControllerAdvice+@ResponseBody,指定要扫描的文件夹下的controller

  • 定义处理方法,使用注解@ExceptionHandler来指明处理的异常对象类型。(Throwable下包含error和exception两个子类)

@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
    @ExceptionHandler(value= Throwable.class)
    public R exceptionHandle(Throwable e) {
        log.error("出现问题:{}0",e.getMessage());
        return R.error(e.getMessage());
    }
}
1
2
3
4
5
6
7
8
  • 常量类定义业务异常状态码。

# 6.Json空字段转换和日期

# 对象字段为空转换Json

jackson包的注解**@JsonInclude(JsonInclude.Include.NON_EMPTY)**,当后端把对象转化Json数据返回时,只有当注解的字段不为空时才把该字段转化Json格式,如果为空,则该对象转化Json时不包含该字段。

不带注解,java对象有字段为空时时,转换的json为:

{"num":1, "child":[]}
1

带了注解后:

{"num":1}
1

# 设置统一日期格式

Springboot默认使用的json解析框架是jackson,使用Date转化Json时可以指定反序列化格式。

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

# 7.后端开发对象类型

  • PO持久对象:对应数据库表中一行记录的所有对应字段,不包含数据库操作
  • TO传输对象:不同服务之间传输的对象,一般类定义在公共依赖中。
  • VO:接收页面传进来的数据,封装对象;将业务处理的对象,封装成页面需要用的数据

# 8.压测

TPS:系统每秒交易数处理数

吞吐量:每秒系统能够处理的请求数

# 优化策略

  • 堆(对象创建):避免FullGC。如果发现老年代和新生代经常饱满,-Xmn指定新生代堆内存大小
    • 新生代:包括一个Eden区,S0,S1
    • 老年代
    • GC过程:创建一个新的对象,首先看Eden区,如果放不下会进行一个小的YoungGC,把Eden中不使用的对象删除,然后看剩下的Eden对象能不能放到S0区(不行放到老年代);如果Eden还是放不下新对象(说明创建的新对象可能占用内存比较大),那么考虑放入老年代;如果老年代还是放不下;那么会进行FullGC。
  • 中间件越多,性能损失越多。
  • 业务
    • DB:①将数据库多次查询变为一次(直接一次查询出一个list,然后封装方法用stream()流一个个取出来,相当于把每次查询的where抽出来由stream做)
    • 静态资源:如果存放在Tomcat中,会导致请求及静态资源时经过的中间件越多,吞吐更低。因此解决方案可以直接将静态资源放在Nginx,用于请求的静态资源经过Nginx直接返回(前后端分离)。

# 压测报错

JMeter Address Already in use:由于windows本身提供的端口访问机制,提供给TCP/IP的端口为1024-5000,并且四分钟才重新回收,这样导致压测时大量请求把端口占满了,个别请求无法发送。

解决:客户端正式释放tcp连接前处于TIME_WAIT状态,会等待2MSL才会关闭连接,因此可以设置为30s,让TCP/IP更快的释放关闭连接,为新连接提供更多的资源;此外还可以设置TCP最大连接数65534。

# 9.缓存

Lettuce和Jedis都是操作redis的底层客户端,其中Lettuce底层基于Netty。最终Spring都会把它们封装成RedisTemplate。

# Redisson-分布式锁

导入依赖,注入redissonClient对象,通过该对象来使用各种redis分布式锁。

  • 可重入锁

场景:A方法调用B方法,A和B都要获取同一把锁。

lock()方法是阻塞式等待方法,获取不到会一直等待,直到获取到锁开始执行业务。不需要自旋锁重复自调用。

内置看门狗自动续上锁的TTL,防止业务执行时间过长;如果lock()自己指定过期时间则看门狗失效。

public void test(){
	RLock lock=redissonClient.getLock("my-lock");//名字相同就是同一把锁
    lock.lock();//加锁,并默认添加TTL
   // lock/lock(10,TimeUnit.SECONDS);
    try{
        //执行业务
    }catch(Exception e){
    }finally{
        lock.unlock();//释放锁
    }
}
1
2
3
4
5
6
7
8
9
10
11
  • 读写锁

向redis中写数据之前添加写锁,在修改数据过程中它是一个排他锁,只能存在一个写锁;读数据之前添加读锁,它是一个共享锁,写锁没释放就必须等待。

  • 闭锁

redissonClient.getCountDownLatch("door"),当分布式闭锁都调用countDown()达到设置次数后,主锁才执行await后业务。

  • 信号量

只有当redis中的信号量value大于0,才能够获取信号量。

缓存数据和数据库一致性问题:数据库写后删缓存模式。实时性一致性要求高的数据就不应该读redis,直接查数据库。缓存只适合于最终一致性,给数据添加过期时间TTL,保证拿到当前最新数据即可。

# SpringCache

替代redis的注解式操作框架

  • 引入依赖spring-boot-starter-cache和redis
  • 配置yml,spring.cache.type=redis
  • 启动类注解@EnableCaching开启缓存

# 10.异步

  • 继承Thread

  • 实现Runnable

  • 实现Callable<class A>:允许run方法有返回值,阻塞等待。通过FutureTask调用

  • 线程池ThreadPoolExecutor(...)

# 线程池

  • corePoolSize:核心线程数,线程池创建好以后准备就绪的线程数量,等待异步任务
  • maximumPoolSize:允许最大线程数量
  • keepAliveTime:存活时间。线程空闲时间大于存活时间,则会释放该空闲线程(核心线程不释放)
  • workQueue:阻塞队列。如果任务有很多,则会把多的任务放在队列。只要有线程空闲,则会去队列里面取出新的任务继续执行
  • threadFactory:线程的创建工厂
  • unit:时间单位
  • RejectExecutionHandler如果阻塞队列也满了,按照指定的拒绝策略拒绝执行任务

整个线程池工作流程:

  1. 首先创建核心线程数,并准备接收任务
  2. 任务过多导致核心线程满了后,把多出来的任务放到阻塞线程中,核心线程执行完一个再从阻塞队列拿一个
  3. 如果此时阻塞队列满了还有新的任务,那么此时创建新的空闲线程,最大不能超过设定的maxSize
  4. 如果创建的新的线程还是满了超过maxsize,则采用拒绝策略拒绝执行任务
  5. maxSize数量线程执行完,空闲线程一旦超过最大存活时间,则删除该空闲线程

# CompletableFuture

启动异步任务,并指定交给哪个线程池执行。

public void test(){
    CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
    sout("执行任务");
    int i=33;
    return i;
    },executor);
    int result=future.get();
}
1
2
3
4
5
6
7
8

组合任务,创建两个CompletableFuture对象,然后可以指定同时完成/顺序执行。

使用CompletableFuture.allof(future1,future2,...)等待多个异步任务全部做完,然后futureAll.get()阻塞等待,都做完才继续下面的逻辑。

public void test(){
    CompletableFuture<Integer> future1;
    CompletableFuture<Integer> future2;
    CompletableFuture<Integer> future3;
    CompletableFuture<Void> allof=CompletableFuture.allof(future1,future2,future3);
    allof.get();//阻塞时等待,才执行下面内容
}
1
2
3
4
5
6
7

所有异步任务方法Api:

  • supplyAsync:包含返回值(供给后面的任务使用)+指定线程池
  • future1.thenAcceptAsync((res)->{}):获取future1的返回值,并串行执行下一个异步任务
  • CompletableFuture.allof(...).get():异步编排所有的future,并等待都执行完(只有调用get主线程才会等待)

# 11.认证服务OAuth2

# 社交登录

使用其它社交软件授权登录。首先第三方应用请求用户认证,用户选择社交登录并输入密码后,会将请求发给服务端的服务器认证,并返回访问令牌(服务端会携带code码信息请求后端的controller,通过回调页的Code码来向服务端请求获取令牌,code码只能用一次)给第三方应用(不直接给所有信息是因为服务端不知道第三方应用需要用户什么信息,不能全给),然后第三方应用携带令牌再向服务端请求对应用户信息,头像,昵称。

  • 首先要在社交服务端申请平台社交登录资格
  • 配置授权回调页(社交验证登录成功跳转到的页面)和取消授权回调页。

image-20230322212837714

# 单点登录

一个应用系统登录后,其它关联的应用系统无需重新登录。比如新浪微博,新浪体育...。

方案:使用xxl-job,同步不同域名系统的用户信息

# 认证授权

场景:①分布式下不同微服务应用不能共享session里的用户信息。②在浏览器不同域名之间进行访问时,cookie里面的Jsession不能同时发送进行共享。子域session要想共享需要在发送时指定浏览器的domain为父域。

方案:前后端分离,用springsecurity生成令牌,存在redis处理用户信息问题。前后端没分离,用springsession处理,也是存redis里,解决session统一共享问题。

# 12.提交订单接口幂等性

去下单—>生成订单数据;提交订单—>提交order订单数据并跳转支付。

用户多次点击提交按钮/用户页面回退后再次提交后,数据库只会提交一份订单数据。

  • token:创建订单对象时,生成一个UUID令牌,一份放入redis,然后另外一份存放在订单对象中。当用户点击提交订单时,从redis取出订单令牌,并和前端传过来的订单对象的令牌作比较,如果相同则向数据库提交订单,并删除redis中的令牌。其中redis令牌的取出,对比和删除必须要保证原子性。
  • 数据库乐观锁机制:适用于读多写少的场景。比如点击按钮减少库存,首先先从库存数据取出version版本号,然后操作数据库前带上该版本号 update set count=count-1,version=version+1 where version=${version},不管点击几次当前获取库存的版本号都是不变的,因此更新操作最终只会被执行一次。

# 13.Aop解决本地事务失效

场景:本地事务是通过代理对象来控制的,当一个对象内不同事务方法互调时,只有最外层的事务方法(调用者)的事务起作用,导致其它被调用方法的事务不起作用。

  • 导入spring-boot-starter-aop依赖,引入aspectj
  • 使用@EnableAspectJAutoProxy(exposeProxy=true)开启aspectj动态代理功能,暴露代理对象。
@Transactional(...)
public void a(){
	OrderServiceImpl o=(OrderServiceImpl)AopContext.currentProxy();
	o.b();
	o.c();
}

@Transactional(...)
public void b(){
}

@Transactional(...)
public void c(){
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 14.分布式事务

场景:方法中其它地方抛出异常,即使添加了@Transactional(只对当前方法内的连接起作用,而对于远程调用服务的数据库连接不起作用),但是不能回滚远程调用已经执行成功的事务。

# Raft算法

节点有三个状态:领导节点,候选人,随从。

  • 领导选举

①首先每个节点的自选时间结束后会变成候选节点,然后候选节点会向其它节点发送投票请求,每个随从节点只能收到一个投票请求;当候选节点收到半数的票后就可以变成领导。(当出现票数一样或者多个候选节点时,则继续进行下一轮票,直到出现竞争出唯一的领导节点)

②每个领导节点根据心跳时间来定时给其它节点发送日志消息,通过心跳机制来维持连接。否则就会删除掉

  • 日志复制

①当领导的数据发生改变时,下一个心跳时会将领导节点的修改日志发送到其它的随从节点,并随从节点的回复;再下一个心跳时领导提交事务(仅当收到大多数节点的回复),然后也通知其它节点提交事务,并通知客户完成更新。

  • 网络分区问题

①另一片分区由于心跳机制收不到领导的心跳后,会重新选举领导。

②当客户端要求数据库操作时,节点少的分区领导因为收不到大多数回复,因此数据提交失败;只有节点数量多的分区才能更新成功。

③网络恢复后,老的领导发现有更高轮选举来的领导会自己退位。然后老领导分区的所有节点事务回滚,匹配新领导的日志。

# seata

  • 首先每个微服务必须创建一个undo_log表
  • 下载seata服务端事务协调器TC,并修改registry.conf注册中心配置文件,file.conf文件配置。启动TC
  • 导入依赖spring-cloud-starter-alibaba-seata,并在需要开启分布式事务的方法标注@globalTransactional
  • 所有想用到分布式事务的微服务需要添加seata代理数据源.

总结:高并发下获取seata全局锁异常,容易导致请求失败。因此一般不用

# 15.延时队列

场景:订单30min未关闭则关闭订单。

  • 在消息中发送对象

可以将对象转换成json数据,然后发送String数据,然后消费者取出数据后需要JSON反序列化

rabbitTemplate.convertAndSend("ex","key",JSON.toJSONString(orderEntity));
1

也可以直接用Spring整合的(启动类注解@EnableRabbie)直接放松一个对象,该对象类需要实现序列化接口,使用java默认的序列化方式

public class Order implements Serializable{}
rabbitTemplate.convertAndSend("ex","key",orderEntity);
1
2
  • 延时队列queue配置
    • x-dead-letter-exchange:死信交换机,当前消息消亡后投递交给哪个交换机
    • x-dead-letter-routing-key:转发到哪个队列的key
    • x-message-ttl:配置消息存活时间
  • 最佳实践

在MQconfig中配置交换机-队列的信息。

创建一个新的消费者对象,类名注解@Component,@RabbitListener,然后在类中的消费逻辑方法注解@RabbitHandler表示消息由哪个方法消费。

  • 整体设计

订单创建后,向死信队列发送消息,并指定死信交换机为当前交换机(只使用一个交换机),当过了设定的TTL时间后,当前交换机根据key投递给订单释放队列,队列监听到消息后检查是否支付。

image-20230323161051746

注意:当消费者消费完后,还可以继续发新的消息以保证业务的串行执行过程。MQ可以替代seata

# 16.消息可靠性

  • 消息丢失

1)使用try-catch,保证消息发送失败后具有重试机制。每个消息日志可以在数据库保存每个发送消息的状态信息(定期扫描数据库进行重发)。

2)消息到达broker代理时没有成功持久化:发送者使用回调机制,如果失败则修改数据库消息状态。

3)消费者开启手动Ack机制,消息消费完才发送确认信息,让MQ删除消息。

  • 消费重复:消费者消费完消息后宕机,发送ack失败,导致MQ重新投递消息给其它消费者消费。

消费业务逻辑设置成幂等性消费:

1)设置数据库索引id或者键值

2)乐观锁(设置state状态位)

3)使用redis生成全局业务id,消息消费完后删除缓存。

  • 消息挤压

1)开启更多的消费者

2)开启离线服务,先把消息批量取出来,存放在数据库,然后离线慢慢处理。

# 17.第三方应用支付-加密算法

使用沙箱环境模拟具体环境。

# 对称/非对称加密

对称加密:发送方和接收方使用同一把密钥来对信息加密解密。

非对称加密:加密解密使用不同的密钥对

image-20230323190446357

用户商家需要配置商户私钥和支付宝公钥。

# 内网穿透

商家还需要配置一个支付成功后的同步通知网页和异步通知网页,这个地址需要被外网正常访问,而不能直接访问到服务器内网。

  • 注册域名或者公网ip服务器
  • 内网穿透:使用一个内网穿透服务商来配置连接到内网服务器。相当于NAT路由器,nginx代理。这样外网访问时先找到外网的内网穿透服务商,然后在进行地址映射。外网访问到本机内网地址。

# 异步通知

当用户支付成功后,支付宝会不间断的持续给异步通知地址发送请求,商家(后端服务器)需要处理该请求并返回输出"success",支付宝才会停止发送异步。

# 18.秒杀服务

# 秒杀预热

使用Cron表达式来指定定时任务的执行时间,定时从数据库查询需要上架的秒杀信息,提前把秒杀商品信息和库存加入缓存。(需要分场次进行保存)

  • 使用spring的异步+定时任务实现定时任务不阻塞。首先在配置类或者定时任务类上注解@EnableAsync,@EnableScheduling,然后在需要开启异步任务的方法上注解@Async,**@Scheduled(cron=...)**开启定时任务

  • 提前将秒杀商品信息和库存放到redis中

  • 使用Redisson的分布式信号量作为秒杀库存数量,可以进行限流

Rsemaphore semaphore=redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+sku_code);
semaphore.trySetPermits(seckillCount);//设置库存数量
semaphore.acquire(num);//阻塞式信号量减一
semaphore.tryAcquire(num);//非阻塞式拿到信号量
1
2
3
4
  • 商品上架时,使用分布式锁redissonClient.getLock("")保证只需要一个微服务上架。

# 秒杀链接加密

防止恶意攻击,获取秒杀链接开启脚本提前发送秒杀请求。在秒杀商品信息对象中设置一个code随机码属性,来判断当前请求是否在秒杀时间之内,如果没有则不能进行秒杀

  • 查询秒杀商品信息—判断场次:使用redisTemplate.keys(SESSIONS_PREFIX+"*")可以直接匹配以session_seckill开头的所有key。获取所有场次的时间区间后,判断当前时间是否在某个区间,如果匹配则可以获取这个key内的当前场次信息value。
  • 前端用户点击查看商品详情时(根据是否是秒杀商品来变化前端按钮是“秒杀抢购”还是“点击购买”),后端判断当前商品在当前时间是否有秒杀场次,如果有则给返回前端商品数据的code码添加上redis中秒杀商品信息的随机码;如果没有则直接给code码置空。

# 异步处理订单信息

判断用户秒杀资格后—>获取redis信号量减缓存,然后创建用户和订单号等等信息,这时候直接给MQ发送信息让其它服务进行异步处理修改数据库。

  • 秒杀资格验证—用户一人一单:用户成功秒杀一单后,就在redis中存放(秒杀信息用户ID_场次ID_商品ID,购买数量),并把当前活动的结束时间设置为过期时间。

image-20230324114428893

# 19.部署配置

elasticsearch:分片式,所有数据分片区(节点)存储,片区之间还可以进行备份(同一份数据在两个不同节点存储)

mysql:主从复制,读写分离。

redis-cluster:将16384个槽位分配到五个节点中,向redis存放数据时,首先根据哈希算法计算key对应的槽位,然后存放入对应的节点中。

vscode快捷键:Alt+Shift+F:代码格式化

编辑 (opens new window)
上次更新: 2023/12/15, 15:49:57
微服务组件快速使用
外卖

← 微服务组件快速使用 外卖→

Theme by Vdoing | Copyright © 2023-2024 blageCoder
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式