问题4-10

问题4.session存储到redis中,保存实体内容进入redis报错。

解决方案

解决1:保存的数据实体(MemberResponseVo)实现implements Serializable。

解决2:其实我们还可以使用JSON的序列化方式来序列化对象到Redis中。直接 JSON.toJSONString(MemberResponseVo);

总结:

Serializable这个接口其实是个空接口。当我们让实体类实现Serializable接口时,其实是在告诉JVM此类可被序列化,可被默认的序列化机制序列化,不需要我们实现

问题5.多系统-单点登录问题(分布式单点登录框架:xxl-sso)

一、windows文件夹里测试sso项目

  1. 1.下载xxl-sso项目,修改为本地配置,并且修改本地hosts地址,域名都指向127.0.0.1
  2. 2.当前文件夹下cmd,然后输入打包命令:mvn clean package -Dmaven.skip.test=true(打包跳过测试)
  3. 3.启动jar包,启动tagret下jar包,启动命令:java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
  4. 4.登录http://ssoserver.com:8080/xxl-sso-server/login
  5. 5.模拟启动另外两个web服务,java -jar xxxx --server.post=8081 java -jar xxxx --server.post=8082

二、单点登录流程

image.png

1.clent1去登录,跳转到登录页面接口login.html,判断是否携带cookie,没有就重定向到登录页面,输入密码登录

  • 2.账号密码校验成功后
    1).把用户信息存到redis中
    2).根据uuid生成一个cookie,放入cookie中( response.addCookie(sso_token);)浏览器在当前域名会话上保存cookie。

3.client1后面直接拿token判断是否登录成功,如果判断登录成功后,直接用token去redis拿用户信息,然后存到当前服务session中,
自己系统将用户保存在自己的会话中,最后页面直接取session中的数据。(session不能跨域)

4.其它client2登录的时候,由于浏览器已经保存了cookie,可以直接免登录。如果cookie过时失效,则重新登录。

问题6.购物车添加-重复刷新页面提交,商品数量增加问题。

解决方案:重定向新页面,刷新页面就不会添加数据

解决:/addCartItem接口,添加成功后,不应该直接展示成功页面,让他重定向到成功页面,这样我们成功添加以后就是跳转到了一个新页面,这样就可以只刷新页面不添加数据。

/**
     * 添加商品到购物车
     * attributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
     * attributes.addAttribute():将数据放在url后面
     * @return
     */

问题7.订单服务-feign远程调用丢失请求头(Cookie)问题

场景问题:

image.png

auth服务登录成功,订单服务跳转到结算页面,浏览器发送请求时,自动带了Cooike,然后订单服务需要调用其他远程服务获取数据,但进入购物车服务时,没有用户信息

解决方案:加feign远程调用请求拦截器

加上 feign 远程调用的请求拦截器,在每次发送远程请求之前,把老请求的数据同步过来,这样就可以解决请求头的丢失问题了。

@Configuration
public class FeignConfig {
    /**
     * 解决远程服务丢失请求头的问题
     * fix #I414SW
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return template -> {
            /*
                通过请求的上下文环境,拿到刚进来的请求数据
             */
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                if (request != null) {
                    // 将老请求的请求头放到新请求的请求头里面
                    template.header("Cookie", request.getHeader("Cookie"));
                }
            }
        };
    }
}

问题8.订单服务-异步任务丢失上下文问题

场景问题:

image.png

获取address、cart所使用的线程,与主任务的线程不同,所以异步任务无法获取主任务的上下文环境。(重新开启线程时,新的线程没有上下文环境信息)

image.png

解决方案:拿到原线程上下文,在新线程重新设置

将主线程中原始请求的上下文数据共享出来,在新开启的异步任务中重新设置上下文数据,即可解决


        // 1.共享主线程中拿到原请求数据
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 查询会员服务地址信息
        CompletableFuture<Void> memberFuture = CompletableFuture.runAsync(() -> {
            // 2.重新设置异步任务的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            R r = memberFeignService.getAddress(memberTO.getId());
            if (r.getCode() == 0) {
                List<OrderConfirmVO.MemberAddressVO> address = r.getData(new TypeReference<List<OrderConfirmVO.MemberAddressVO>>() {
                });
                orderConfirmVO.setAddress(address);
            }
        }, executor);

问题9:提交订单(重复提交)-幂等性问题

场景问题:

订单服务-提交订单按钮需要防止重复提交,造成生成多条订单?

解决方案:token 机制

  1. 服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
  2. 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  3. 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
  4. 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
//一、调用跳转订单确认页面接口时-生成防重令牌,并返回前端页面
String orderToken = UUID.randomUUID().toString().replace("-", "");
orderConfirmVO.setOrderToken(orderToken);
stringRedisTemplate.opsForValue().set(OrderConstant.ORDER_TOKEN_PREFIX + memberTO.getId(), orderToken, 30,
                TimeUnit.MINUTES);

//二、调用提交订单接口时,需提交防重令牌(token)
String orderToken = orderSubmitVO.getOrderToken();
//Token 获取、比较和删除必须是原子性 在redis 使用 lua 脚本完成这个操作
// 这段脚本的意思是,如果获取key对应的value是传过来的值,那就调用删除方法返回1,否则返回0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 原子删锁
 /*
 实现RedisScript接口的实现类(脚本内容,执行完之后的返回值类型)
 一个存key的list
 要对比的value
 */
Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
 Arrays.asList(OrderConstant.ORDER_TOKEN_PREFIX + memberTO.getId()), orderToken);
// 1、校验令牌
if (result == 0L) {
// 验证失败
responseVO.setCode(1);
} else {
// 验证成功
}

问题10.订单服务调用feign远程服务,出现异常,事务如何保证?

场景问题

image.png

场景1:远程服务其实成功了,由于网络故障等没有返回 导致:订单回滚,库存却扣减

场景2:远程服务执行完成,下面的其他方法出现问题 导致:已执行的远程请求,肯定不能回滚。因为远程服务调用的数据库都不一样,建立的一个新连接,肯定不能回滚。

image.png

解决方案:利用消息队列实现最终一致

库存服务锁定成功后发给消息队列消息(当前库 存工作单),过段时间自动解锁,解锁时先查询 订单的支付状态。解锁成功修改库存工作单详情 项状态为已解锁