黑马点评-实现安全秒杀优惠券(使并发一人一单,防止并发超卖)
一.实现优惠券秒杀
1.最原始代码:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 结束return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}//5,扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder); // 写入数据库return Result.ok(orderId);}
}
出现的问题:在高并法的情况下存在多买的情况,导致商品成为负数
代码出现问题的原理图:
2.使用乐观锁解决超卖的问题
在修改时,查看之前查出来的数据和现在的数据库里面的优惠券的数量是否相同,相同就表示期间没有修改,则我们可以执行减库存的操作。
代码修改:
// 把上面的sql语句改成这个
boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).eq("stock",voucher.getStock()) // 添加乐观锁,但是成功率太低.update();
出现的问题:200个用户抢100个优惠券这100个优惠券还有剩余。
3.优化乐观锁
在修改时,其实我们只用查看库存剩余的券的数量是否大于0就可以执行减库存了,不需要向上面一样写的这么严格。
代码修改:
// 把上面的sql语句改成这个
boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0) // 优化乐观锁.update();
二.实现一人一单
1.有问题的代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 结束return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}//5,扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).eq("stock",voucher.getStock()) // 添加乐观锁,但是成功率太低.update();if (!success) {//扣减库存return Result.fail("库存不足!");}// 6. 一人一单Long userId = UserHolder.getUser().getId();// 6.1. 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 6.2. 判断是否存在if(count > 0){// 用户已经购买过了return Result.fail("用户已经购买过一次!");}//7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder); // 写入数据库return Result.ok(orderId);}
}
出现的问题:在高并发的情况下还是会出现一个人可以下多单
出现问题的原理图:
2.使用悲观锁:
更改步骤5之后的逻辑,添加synchronized
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {// 5. 一人一单Long userId = UserHolder.getUser().getId();// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了return Result.fail("用户已经购买过一次!");}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);
}
但是这样添加锁,锁的粒度太粗了,在使用锁过程中,控制 锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度。
3.优化悲观锁的使用
思路:我们对用户的id进行上锁。但是,注意这里toString.intern,这里和tostring的底层原理有关使用了这个相当于创建了一个新的对象,所以同一id不会被锁住,加了intern就会去string的常量池里面去找,就不会创建新的了
@Transactional
public Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()){// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了return Result.fail("用户已经购买过一次!");}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}
}
4.再次优化
但是这里我们还需要注意,这里是先释放锁在提交事务的,可能会出现问题,所以我们要把函数里面的锁去掉,在调用的地方加锁即可
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {return this.createVoucherOrder(voucherId);
}
5.继续优化,前面的修改会导致事务失效
所以我们要获取代理对象并用代理对象调用 createVoucherOrder 方法
关于事务失效可以查看这篇博客 点击进入
如果还是不清楚可以查看一人一单视频的最后一点
synchronized (userId.toString().intern()) {// 获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}
// 这里还需要在启动类上面添加这个注解,去暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
出现的问题:在多节点部署服务器的时候,还是会出现一人多单的问题,因为每个jvm的锁的监视器不同导致,无法锁住导致一人多单,这里就需要分布式锁
出现问题的原理图:
6.使用分布式锁解决集群部署的并发问题
解决问题的原理图:
具体实现可以查看这篇博客 点击进入
7.使用异步的思路解决高并发
实现异步的思路的流程图:
① 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
// 改造添加优惠券代码
@Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀库存到Redis中stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
}
② 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
lua脚本实现思路的流程图:
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.库存不足,返回1return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,说明是重复下单,返回2return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
③ 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
public class VoucherOrderServiceImpl {private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024);private IVoucherOrderService proxy; // 保存这个类的代理对象public Result seckillVoucher(Long voucherId) {//获取用户Long userId = UserHolder.getUser().getId();// 1.执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());int r = result.intValue();// 2.判断结果是否为0if (r != 0) {// 2.1.不为0 ,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 2.2. 为0,有购买资格,把下单信息保存到阻塞队列VoucherOrder voucherOrder = new VoucherOrder();// 2.3.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 2.4.用户idvoucherOrder.setUserId(userId);// 2.5.代金券idvoucherOrder.setVoucherId(voucherId);// 2.6.放入阻塞队列orderTasks.add(voucherOrder);// 3.获取代理对象,保证后面线程池中使用代理对象去完成消费阻塞队列里面的消息proxy = (IVoucherOrderService)AopContext.currentProxy();// 4.返回订单idreturn Result.ok(orderId);}
}
④ 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
@Slf4j
public class VoucherOrderServiceImpl {//异步处理线程池private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();private IVoucherOrderService proxy;@PostConstruct // 当前类初始化完毕就执行private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable {public void run() {while (true) { // 一直去阻塞队列获取消息try {// 1.获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();// 2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {// 1.获取用户Long userId = voucherOrder.getUserId();// 2.创建锁对象RLock redisLock = redissonClient.getLock("lock:order:" + userId);// 3.尝试获取锁boolean isLock = redisLock.lock();// 4.判断是否获得锁成功if (!isLock) {// 获取锁失败,直接返回失败或者重试log.error("不允许重复下单!");return;}try {//注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效proxy.createVoucherOrder(voucherOrder);} finally {// 释放锁redisLock.unlock();}}@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了log.error("用户已经购买过了");return ;}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败log.error("库存不足");return ;}// 7.创建订单save(voucherOrder);}
}
存在的问题:上面的阻塞队列在jvm中,会存在内存限制的问题,如果服务器宕机了,就会导致数据全部消失。
然后我们可以使用redis中的stream这个数据结构作为消息队列进行优化,后面优化的代码就不写了,因为要使用消息队列的话还是建议使用 rabbitmq 这种开源的消息队列组件就行了,这里我们只需要懂思想了。
相关文章:

黑马点评-实现安全秒杀优惠券(使并发一人一单,防止并发超卖)
一.实现优惠券秒杀 1.最原始代码: Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {Resourceprivate ISeckillVoucherService seckillVoucherService;Resourcepriv…...

解决论文中字体未嵌入的问题
文章总览:YuanDaiMa2048博客文章总览 解决论文中字体未嵌入的问题 问题描述解决方案:使用 Adobe PDF 打印机嵌入字体(WPS版)步骤一:打开 PDF 文件步骤二:选择打印到 Adobe PDF步骤三:修改 Adobe…...

leetcode 131. Palindrome Partitioning
目录 一、题目描述 二、方法1、回溯法每次暴力判断回文子串 三、方法2、动态规划回溯法 一、题目描述 分割回文子串 131. Palindrome Partitioning 二、方法1、回溯法每次暴力判断回文子串 class Solution {vector<vector<string>> res;vector<string>…...
Android本地语音识别引擎深度对比与集成指南:Vosk vs SherpaOnnx
技术选型对比矩阵 对比维度VoskSherpaOnnx核心架构基于Kaldi二次开发ONNX Runtime + K2新一代架构模型格式专用格式(需专用工具转换)ONNX标准格式(跨框架通用)中文识别精度89.2% (TDNN模型)92.7% (Zipformer流式模型)内存占用60-150MB30-80MB迟表现320-500ms180-300ms多线程…...

审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验
在审计工作中,Word附注通常包含很多表格。为了确保附注数字的准确性,我们需要对这些表格进行数字逻辑校验,主要包含两个维度:在纵向上验证合计项金额是否正确;在横向上检查“年末金额年初金额本期增加-本期减少”的勾稽…...

人工智能数学基础实验(四):最大似然估计的-AI 模型训练与参数优化
一、实验目的 理解最大似然估计(MLE)原理:掌握通过最大化数据出现概率估计模型参数的核心思想。实现 MLE 与 AI 模型结合:使用 MLE 手动估计朴素贝叶斯模型参数,并与 Scikit-learn 内置模型对比,深入理解参…...

告别延迟!Ethernetip转modbustcp网关在熔炼车间监控的极速时代
熔炼车间热火朝天,巨大的热风炉发出隆隆的轰鸣声,我作为一名技术操控工,正密切关注着监控系统上跳动的各项参数。这套基于EtherNET/ip的监控系统,是我们车间数字化改造的核心,它将原本分散的控制单元整合在一起&#x…...
Kotlin协程优化Android ANR问题
引言 在Android开发中,ANR(Application Not Responding)是用户体验的致命杀手。当主线程被耗时操作阻塞超过阈值(5秒前台/10秒后台),系统会直接弹窗提示应用无响应。本文将深入剖析如何通过Kotlin协程将耗…...

Visual Studio Code插件离线安装指南:从市场获取并手动部署
Visual Studio Code插件离线安装指南:从市场获取并手动部署 一、场景背景二、操作步骤详解步骤1:访问官方插件市场步骤2:定位目标版本步骤3:提取关键参数步骤4:构造下载链接步骤5:下载与安装 三、注意事项 …...
构建安全AI风险识别大模型:CoT、训练集与Agent vs. Fine-Tuning对比
构建安全AI风险识别大模型:CoT、训练集与Agent vs. Fine-Tuning对比 安全AI风险识别大模型旨在通过自然语言处理(NLP)技术,检测和分析潜在的安全威胁,如数据泄露、合规违规或恶意行为。本文从Chain-of-Thought (CoT)设计、训练集构建、以及Agent-based方法与**AI直接调优…...

计算机视觉---YOLOv1
YOLOv1深度解析:单阶段目标检测的开山之作 一、YOLOv1概述 提出背景: 2016年由Joseph Redmon等人提出,全称"You Only Look Once",首次将目标检测视为回归问题,开创单阶段(One-Stage)…...
无法同步书签,火狐浏览器修改使用国内的账号服务器
自动更新版本后,变为国际服版本的了,点击右上角无法登录firefox,也无法同步书签,现在国际服的火狐浏览器修改使用国内的账号服务器,需要先在搜索框输入 about:config 中改变三项配置,然后重启浏览器,才能正常使用国内的火狐账号服务器 ident…...
动态防御体系实战:AI如何重构DDoS攻防逻辑
1. 传统高防IP的静态瓶颈 传统高防IP依赖预定义规则库,面对SYN Flood、CC攻击等常见威胁时,常因规则更新滞后导致误封合法流量。例如,某电商平台遭遇HTTP慢速攻击时,静态阈值过滤无法区分正常用户与攻击者,导致订单接…...
Kotlin Native与C/C++高效互操作:技术原理与性能优化指南
一、互操作基础与性能瓶颈分析 1.1 Kotlin Native调用原理 Kotlin Native通过LLVM编译器生成机器码,与C/C++的互操作基于以下核心机制: CInterop工具:解析C头文件生成Kotlin/Native绑定(.klib),自动生成类型映射和包装函数双向调用约定: Kotlin调用C:直接通过生成的绑…...

爬虫核心概念与工作原理详解
爬虫核心概念与工作原理详解 1. 什么是网络爬虫? 网络爬虫(Web Crawler)是一种按照特定规则自动抓取互联网信息的程序或脚本,本质是模拟人类浏览器行为,通过HTTP请求获取网页数据并解析处理。 形象比喻:如…...
Flink架构概览,Flink DataStream API 的使用,FlinkCDC的使用
一、Flink与其他组件的协同 Flink 是一个分布式、高性能、始终可用、准确一次(Exactly-Once)语义的流处理引擎,广泛应用于大数据实时处理场景中。它与 Hadoop 生态系统中的组件可以深度集成,形成完整的大数据处理链路。下面我们从…...

vue3前端后端地址可配置方案
在开发vue3项目过程中,需要切换不同的服务器部署,代码中配置的服务需要可灵活配置,不随着run npm build把网址打包到代码资源中,不然每次切换都需要重新run npm build。需要一个配置文件可以修改服务地址,而打包的代码…...
Es6中怎么使用class实现面向对象编程
在 JavaScript 中,面向对象的类可以通过 class 关键字来定义。以下是一个简单的示例,展示了如何定义一个类、创建对象以及添加方法: 基础类定义 // 定义一个类 class MyClass { // 构造函数,用于初始化对象的属性 constructor(pa…...

digitalworld.local: FALL靶场
digitalworld.local: FALL 来自 <digitalworld.local: FALL ~ VulnHub> 1,将两台虚拟机网络连接都改为NAT模式 2,攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182,靶场IP192.168.23.4 3&…...

MySQL---库操作
mysql> create database if not exists kuku3; 1.库操作的语法 create database [if not exists] db_name [create_specification [, create_specification] ...] create_specification: [default] character set charset_name [default] collate collation_name详细解释…...

动态规划算法:字符串类问题(2)公共串
0 前言 上节课我们已经讲述了使用动态规划求取回文串长度与数量的方法(和本节课关系不大,感兴趣可以去看字符串类问题(1)回文串),这节课我们继续探索字符串问题中的动态规划问题。 进入本篇文章前&#x…...
uni-app(5):Vue3语法基础上
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统,只关注视图层,…...

深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化,全面掌握性能优化核心技巧
深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化,全面掌握性能优化核心技巧 一、分包核心价值与基本原理 1.1 为什么需要分包 首屏加载优化:减少主包体积,提升TTI(Time to Interactive)缓存利用率提升&am…...
ubuntu下docker安装mongodb-支持单副本集
1.mogodb支持事务的前提 1) MongoDB 版本:确保 MongoDB 版本大于或等于 4.0,因为事务支持是在 4.0 版本中引入的。 2) 副本集配置:MongoDB 必须以副本集(Replica Set)模式运行,即使是单节点副本集&#x…...

spring-boot-starter-data-redis应用详解
一、依赖引入与基础配置 添加依赖 在 pom.xml 中引入 Spring Data Redis 的 Starter 依赖,默认使用 Lettuce 客户端: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis<…...

5060显卡驱动PyCUDA开发环境搭建
5060显卡驱动PyCUDA开发环境搭建 本文手把手讲解了RTX5060ti显卡从上手尝试折腾,到在最新Ubuntu LTS版本上CUDA开发环境搭建成功的详细流程。 1.1 开机后Ubuntu2404LTS不识别显卡 1.1.1 显卡硬件规格要求 笔者下单的铭瑄电竞之心,安装规格是PCIe …...

redis搭建最小的集群,3主3从
create.sh脚本用于快速部署一个Docker化的Redis集群。首先,脚本创建了一个自定义的Docker网络redis-net,并指定了子网以防止IP变动。接着,脚本设置了宿主机的公网IP,并生成了六个Redis节点的配置文件,每个配置文件都启…...
《帝国时代1》游戏秘籍
资源类 PEPPERONI PIZZA:获得 1000 食物。COINAGE:获得 1000 金。WOODSTOCK:获得 1000 木头。QUARRY:获得 1000 石头。 建筑与生产类 STEROIDS:快速建筑。 地图类 REVEAL MAP:显示所有地图。NO FOG…...

【sylar-webserver】10 HTTP模块
HTTP 解析 这里使用 nodejs/http-parser 提供的 HTTP 解析器。 HTTP 常量定义 HttpMethod HttpStatus /* Request Methods */ #define HTTP_METHOD_MAP(XX) \XX(0, DELETE, DELETE) \XX(1, GET, GET) \XX(2, HEAD, HEAD) …...
攻略生成模块
攻略生成模块 这个模块实现了一个旅行行程规划服务,主要流程如下: 核心思路是通过前端传来的城市和出游天数信息,先在本地数据库中查找是否已存有相应的旅游数据(例如景点、美食等),如果没有就自动检索和…...