项目细节 3

幂等性

SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性
insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性。 UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性

幂等解决方案
1、token 机制

  • 1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
  • 2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  • 3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业 务。
  • 4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样 就保证了业务代码,不被重复执行。

危险性:

  • 1、先删除 token 还是后删除 token; (1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致, 请求还是不能执行。 (2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别 人继续重试,导致业务被执行两边 (3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
  • 2、Token 获取、比较和删除必须是原子性 (1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导 致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行 (2) 可以在 redis 使用 lua 脚本完成这个操作
    1. if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

2、各种锁机制

  • 1、数据库悲观锁
  • 2、数据库乐观锁
  • 3、业务层分布式锁

3、各种唯一约束

  • 1、数据库唯一约束

这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键 的要求不是自增的主键,这样就需要业务生成全局唯一的主键
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要 不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

  • 2、redis set 防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set, 每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。

4、防重表

使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且 他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避 免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个 事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

5、全局请求唯一 id

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id; proxy_set_header X-Request-Id $request_id;

        //4. 页面设置防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespTo.getId(), token, 30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


// === 提交订单的时候 获取key教研
//验证通过[保证令牌 删除 的 原子性]
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        // 原子验证和删除
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                , Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespTo.getId())
                , orderToken);
// 创建36位长度的订单号,按照时间递增
IdWorker.getTimeId() 
//验证价格的时候的注意点, 页面提交的价格可能是BigDeimal:8.31,后台计算的价格可能是8.311111
// 所以只要两个直接的<0.01就算 成功

锁库存
在这里插入图片描述

 UPDATE wms_ware_sku SET stock_locked=stock_locked+#{quantity}
        WHERE sku_id=#{skuId} AND stock-stock_locked>=#{quantity}

本地事务在分布式情况下还是有问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

0)、导入 spring-boot-starter-aop 
1)、@EnableTransactionManagement(proxyTargetClass = true) 
2)、@EnableAspectJAutoProxy(exposeProxy=true) 
3)、AopContext.currentProxy() 调用方法

3、分布式事务几种方案

1)、2PC 模式

第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是 否可以提交.
第二阶段:事务协调器要求每个数据库提交数据。 其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。
在这里插入图片描述

  • XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较 低。
  • XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景
  • XA 目前在商业数据库支持的比较理想,在 mysql 数据库中支持的不太理想,mysql 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。
  • 许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。
  • 也有 3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间 未收到回应则做出相应处理)

2)、柔性事务-TCC 事务补偿型方案

柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
在这里插入图片描述

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。 .
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
  • 所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
    在这里插入图片描述

3)、柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种 方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调

4)、柔性事务-可靠消息+最终一致性方案(异步确保)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

防止消息丢失: 
/*** 1、做好消息确认机制(pulisher,consumer【手动 ack】) *
 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一 遍
 */

Seata

  1. 为每个服务创建UNDO_LOG表
  2. 安装seata server
  3. registry.conf:配置中心配置, 修改registry type =nacos
  4. 想要用到分布式事务的微服务使用seata代理
  5. 每个项目需要在resources放 registry.conf和file.conf
  6. 在这里插入图片描述
  7. 分布式大事务的入口标注@GlobalTransactional,远程的小事务使用@Transactional
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;


    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {

        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }

        return new DataSourceProxy(dataSource);
    }
}

MQ的延时队列 来解决最终一致性

消息的TTL和死信队列
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

支付寶回調

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

秒杀

提前把商品信息放到redis中
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 定时任务上架
 /**
     * 上架最近三天的秒杀商品
     *
     * 当天 0:00-3:00
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void uploadSeckillSkuLatest3Day(){
        //正在上架商品
        log.info("正在上架商品");
        //加上一个分布式锁
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try{
            seckillService.uploadSeckillSkuLatest3Day();
        }finally {
            lock.unlock();
        }
    }



    @Override
    public void uploadSeckillSkuLatest3Day() {
        List<SeckillSessionsWithSkus> sessionData;
        R r = couponFeignService.getSKuLatest3Day();
        if (r.getCode() == 0) {
            //成功
            sessionData = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });

            //缓存信息
            //1.缓存活动信息
            //2.缓存关联的商品信息
            saveSessionInfos(sessionData);
            saveSessionSkuInfo(sessionData);
        }
    }


    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessionData) {
        sessionData.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();

            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;

            Boolean isExt = redisTemplate.hasKey(key);
            if (!isExt) {
                List<String> skuIds = session.getRelationSkus().stream().map(item -> {
                    //TODO 老师的是item.getId()
                    return item.getPromotionSessionId() + "_" + item.getSkuId().toString();
                }).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, skuIds);
            }


        });
    }

    private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessionData) {
        //准备hash操作
        BoundHashOperations ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        sessionData.stream().forEach(session -> {
                    session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                        SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                        String token = UUID.randomUUID().toString().replace("_", "");
                        if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {
                            //缓存商品
                            //1.sku的基本信息
                            BeanUtils.copyProperties(seckillSkuVo, redisTo);
                            //2.sku的秒杀信息
                            R r = productFeignService.getSKuInfo(seckillSkuVo.getSkuId());
                            if (r.getCode() == 0) {
                                SkuInfoVo skuInfoVo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                                });
                                redisTo.setSkuInfo(skuInfoVo);
                            }
                            //3.保存随机码,结束时间
                            redisTo.setStartTime(session.getStartTime().getTime());
                            redisTo.setEndTime(session.getEndTime().getTime());

                            redisTo.setRandomCode(token);
                            String s = JSON.toJSONString(redisTo);
                            ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), s);

                            //5.使用Redisson限流(RSemaphore)  根据商品库存 限制数量
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                            semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                        }
                    });
                }
        );

    }

在这里插入图片描述
在这里插入图片描述

 @Override
    public String kill(String killId, String key, Integer num) {
        MemberRespTo memberRespTo = LoginInterceptor.threadLocal.get();
        //1.获取当前商品的秒杀信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        } else {
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            //合法性校验
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            Long now = System.currentTimeMillis();
            //1.校验时间的合法性
            if (now > startTime && now < endTime) {
                //在秒杀时间内
                //2.校验随机码
                String randomCode = redisTo.getRandomCode();
                String id = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                if (randomCode.equals(key) && id.equals(killId)) {
                    //数据合法
                    //3.验证购物数量是否合理
                    if (num <= redisTo.getSeckillLimit()) {
                        //4.验证这个商品是否购买过
                        String redisKey = memberRespTo.getId() + "_" + redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                        //自动过期,
                        Long ttl = endTime - now;
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功过,之前没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);

                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                //秒杀成功
                                //快速下单
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setMemberId(memberRespTo.getId());
                                orderTo.setOrderSn(timeId);//订单号
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());//设置活动id
                                orderTo.setNum(num);
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                return timeId;
                            }
                            return null;

                        } else {
                            //占位失败,返回为空
                            return null;
                        }

                    }
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }
        return null;
    }

==> 发送到消息队列处理
    @RabbitListener(queues = "order.seckill.order.queue")
    public void listener(SeckillOrderTo seckillOrder, Message message, Channel channel) throws IOException {
        log.info("收到秒杀订单消息,准备执行单->"+seckillOrder.getOrderSn());
        try{
            orderService.createSecOrder(seckillOrder);
            //支付宝手动收单
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }

    }

@Override
    public void createSecOrder(SeckillOrderTo seckillOrder) {
        //TODO 保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrder.getOrderSn());
        orderEntity.setMemberId(seckillOrder.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal(seckillOrder.getNum()));
        orderEntity.setPayAmount(multiply);

        this.save(orderEntity);

        //TODO 保存订单项信息
        OrderItemEntity itemEntity = new OrderItemEntity();
        itemEntity.setOrderSn(seckillOrder.getOrderSn());
        itemEntity.setSkuId(seckillOrder.getSkuId());
        itemEntity.setRealAmount(multiply);
        itemEntity.setSkuQuantity(seckillOrder.getNum());
        //TODO 通过feign来获取sku信息

        orderItemService.save(itemEntity);
    }

sentinel

# 控制台端口
spring.cloud.sentinel.transport.dashboard=localhost:8084
# 数据传输端口
spring.cloud.sentinel.transport.port=8719

management.endpoints.web.exposure.include=*
// 自定义 响应页面
@Configuration
public class MySentinelConfig {
    public MySentinelConfig() {
        WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler(){
            @Override
            public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
                R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
                response.setCharacterEncoding("utf-8");
                response.setContentType("application/json");
                response.getWriter().write(JSON.toJSONString(error));
            }
        });
    }
}

在这里插入图片描述

对feign的熔断

// 调用方
feign.sentinel.enabled=true

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

与网关结合

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>

在这里插入图片描述

@Configuration
public class SentinelGateWayConfig {
    public SentinelGateWayConfig() {
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
                R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
                String errorJson= JSON.toJSONString(error);
                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errorJson), String.class);
                return body;
            }
        });
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值