电商平台中订单未支付过期如何实现自动关单?
定时任务
适用场景:订单取消操作复杂,需要分布式支持。
优点:
- 成熟的调度框架支持复杂任务调度。
- 灵活性高,支持分布式扩展。
缺点:
- 对实时性支持有限。
- 框架本身较复杂。
延迟消息队列
在订单创建时,发送一个消息到延迟队列(延迟消息),延迟队列的时间为支付超时时间。当消息被消费时,如果订单仍未支付,则关闭订单。
适用场景:适用高并发系统,实时性要求高。
优点:
- 消息队列支持分布式,高并发下表现优秀。
- 数据可靠性高,不容易丢消息。
缺点:
- 引入消息队列增加了系统复杂性。
- 需要处理队列堆积的问题。
RedisKey过期监听
在创建订单时,写入一个定时过期的key,当key过期时,监听到redis的keyevent 事件通知,进行后续处理。当前这种方式不能作为唯一,还需在重启服务时做一次扫库,避免因发版本等异常情况而导致未监听到key过期。
适用场景:对超时事件实时性要求高,并且希望依赖 Redis 本身的特性实现简单的任务调度。
优点:
- 实现简单,直接利用 Redis 的过期机制。
- 实时性高,过期事件触发后立即响应。
缺点:
- 依赖 Redis 的事件通知功能,需要开启 notify-keyspace-events 配置。
- 如果 Redis 中大量使用过期 Key,可能导致性能问题。
如何在100亿数据中找到中位数
如果内存足够,直接在内存中进行快排即可。
如果内存不够,可以用桶排序。这个需要先知道数据的范围,例如数据是1~100之间。可以创建十个桶,1到10、11到20等等
1)创建多个小文件桶,设定每个桶的取值范围,然后把海量数据元素根据数值分配到对应的桶中,并记录桶中元素的个数
2)根据桶中元素的个数,计算出中位数所在的桶(比如 100 亿个数据,第 1 个桶到第 18 个桶一共有 49 亿个数据,第 19 个桶有 2 亿数据,那么中位数一定在第 19 个桶中),然后针对该桶进行排序,就可以求出海量数据中位数的值(如果内存还是不够,可以继续对这个桶进行拆分;或者直接用 BitMap 来排序)
参考:
https://www.cnblogs.com/myf008/p/18323329
如果系统QPS突然提升10倍该怎么处理?
预防措施
- 性能监控:首先,建立完善的性能监控体系,实时监控系统额度QPS、CPU、内存、磁盘IO等指标,及时发现和预警性能问题。
- 负载均衡:使用负载均衡中间件(如Nginx)将流量分布到多个节点上,防止某个节点成为瓶颈。
- 部署备用节点:流量突增假如伴随着事件,在事件发布之前做好备用节点,使用zk或者redis的心跳过期监听来启动备用节点,以防节点挂了一个之后导致其他节点成为瓶颈,也有可能会跟着挂掉
- 自动扩容:可以监控核心参数,例如CPU使用率,高于30%自动扩容等。
突发流量处理
- 系统扩容:在突发流量到来时,可通过水平扩展快速提升系统容量:自动扩展:使用容器编排工具(如k8s)配置自动扩展策略,根据流量自动增加或减少服务实例数量。
- 缓存策略:合理使用缓存,能大幅度提升系统性能,减少数据库压力:本地缓存:可以使用guava或caffeine等本地缓存库缓存热点数据。分布式缓存:使用Redis缓存高频访问的数据。
- 限流与降级:限流:使用限流算法(如令牌桶、漏桶)限制每秒请求数,保护系统免受流量冲击。降级:对非核心功能进行降级处理,确保核心功能的稳定性
原文链接:https://blog.youkuaiyun.com/qq_35551875/article/details/140842405
实现一个分布式锁需要注意什么
使用什么介质存储
可以使用内存存储,对缓存来说非常快。
过期策略
- 定时删除,定时扫描哪些过期了。
- 惰性删除,下次访问时发现过期就删除。
- 到时间点删除,即定义一个监控线程,到点删除。
自动延期策略
可以定义一个 TimerTask 的定时异步任务,它继承了Runnable,run里的任务是刷新过期时间。
定义一个 Timer,指定什么时候去执行 TimerTask 这个任务,例如每隔1/5个过期时间执行一次,就可以实现自动延期。
可重入策略
针对每个加锁的地方,需要记录它的id,并且每加一次锁,在redis中对他进行++,每次解锁对他进行–,以此来记录它的重入次数,实现可重入。
记录它的id可以用这样的方式:代码里会有 RedisLock lock = getLock() 的这样的操作,即:针对每个key需要加一个Lock对象,这个对象在生成时会生成自己的id,当做rediskey里的value,生成id的规则:应用id + 本地机器的ip +进程id + 线程id。
为了优化它的速度,可以在 threadLocal 里记录它的可重入次数,同线程内的可重入性用 threadLocal 保证就可以了。
当然这两种方法都无法避免这样的场景:在 主线程thread 里对 key1 加锁,但是 主线程thread 又启动一个子线程 thread2,又在thread2 里对 key1 进行加锁,就会进入死锁。所以不能在子线程里对同一个key加锁。
分布式锁加锁失败后的等待处理策略
- 使用信号量Semaphore,分布式环境下需要使用 DistSemaphore,信号量Semaphore可以实现阻塞和被唤醒。
- 使用轮询,不停轮询,这样实现简单,但是对资源消耗比较高。
- 使用消息通知,释放锁时唤醒其他机器。
什么是信号量?
信号量(Semaphore)是一种计数器,用于控制对共享资源的访问。它维护了一组许可(permits),线程在访问资源之前必须先获取许可,访问完成后释放许可。如果许可数为 0,线程会被阻塞,直到有其他线程释放许可。
信号量的核心操作包括:
- acquire():获取一个许可,如果许可数为 0,则线程阻塞。
- tryAcquire(long timeout, TimeUnit unit):获取一个许可,如果许可数为 0,则线程阻塞。如果超时则返回false。
- release():释放一个许可,唤醒等待的线程。
在分布式环境中,多个节点(JVM 进程)之间需要共享信号量状态,因此需要使用分布式信号量(DistSemaphore)。分布式信号量通常基于分布式协调服务(如 ZooKeeper、Redis、etcd)实现。
举一个分布式信号量的实现的例子:
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class DistSemaphoreExample {
public static void main(String[] args) throws InterruptedException {
// 1. 配置 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 2. 创建 Redisson 客户端
RedissonClient redisson = Redisson.create(config);
// 3. 获取分布式信号量
RSemaphore semaphore = redisson.getSemaphore("mySemaphore");
semaphore.trySetPermits(1); // 设置信号量的许可数为 3
// 4. 申请资源
semaphore.acquire(); // 阻塞直到获取许可
System.out.println("Resource acquired!");
// 5. 释放资源
semaphore.release();
System.out.println("Resource released!");
// 6. 关闭 Redisson 客户端
redisson.shutdown();
}
}
我们可以使用信号量来完成重试,如果获取失败,尝试获取信号量,获取不到信号量,线程会阻塞住,等待持有锁的线程释放信号量,释放信号量时就会唤醒之前获取锁失败的线程。
集群策略,主从同步策略
保证集群的高可用性,主机器坏了可以立马让从机器接替。
数据分片
通过一致性哈希算法,分配缓存键到不用的节点,实现集群的分片和扩张
并发处理策略
- 单线程处理,可以避免并发问题,并且避免频繁的上下文切换,但是不能利用多核机器的资源
- 多程程处理,可以使用版本号机制,可以充分利用多核,缺点是实现复杂,切自旋效率低
接口慢如何排查
排查方向:
- 数据库:是不是有慢SQL,数据库抖动等等
- 外部接口:下游接口响应慢
- 代码问题:代码里有循环、递归等等
- JVM:是不是频繁GC,导致GC线程暂停了业务线程
- 缓存问题:本地缓存失效,访问外部资源慢
- 网络问题:使用ping、traceroute或telnet等工具检查网络连通性和延迟。
- 机器问题:CPU、内存、磁盘I/O、网络等是否正常。
- 线程池:是不是线程池设置不对,比如队列太长,用不到最大线程数
排查方式:
- 日志:查看日志排查
- 机器监控:查看机器性能
- 告警:查看告警
峰值100万QPS的缓存系统如何设计
多级缓存设计
- 本地缓存:拉取Redis缓存,这里需要注意有个缓存一致性的问题。
- Redis缓存:由一个单独的写入系统,将数据库的数据写入Redis缓存(可以是更新时写入,也可以是定时轮询),读取由Redis缓存提供,降低数据库压力。
- 业务上的性能优化:根据大部分请求的规则,将多个缓存聚合在一起返回等等。
分布式缓存数据量太大怎么处理
- 数据压缩:设计一套压缩算法,将业务数据进行压缩,例如boolean类型转为一个byte,字符转如果是枚举,转为枚举等等。
- Redis数据压缩:Redis本身也支持数据压缩。
- 数据分片:通过一致性哈希算法,对数据进行分片。增加更多的缓存节点,通过水平扩展来分担数据存储压力。
- 优化缓存的过期和清理策略:使用合适的缓存淘汰策略(如LRU、LFU)来自动移除不常用的数据。设置合理的过期时间来确保过期的数据及时被清除。
交易系统中如何防止资损的问题
- 幂等性设计,确保资金相关的操作(如支付、退款、转账)是幂等的,即同一请求多次执行的结果与一次执行的结果相同。例如记录退款通知的主键,根据主键做幂等。
- 事务管理,当涉及到多张表的操作时,需要考虑使用事务操作,保证原子性。如果是单机,使用本地事务就可以;如果是多个应用,就需要考虑分布式事务。
- 定时对账:定期与银行、第三方支付平台对账,确保系统数据与外部数据一致。
- 实时对比:通过监控或者是JOB,隔10分钟扫描一次数据,对比自己数据库和外部交易系统的查询接口,保证资金的一致性。
- 审计系统:记录每一条资金变化的交易流水,保证可以追溯,并且通过审计系统(一套单独的校验逻辑,例如根据账户配置计算),保证每条交易流水的正确性。
- 考虑并发问题,加分布式锁,防止并发问题。
- 单元测试,编写各个交易场景的单元测试,防止计算有问题。
- 监控告警,增加监控告警,防止没有长时间没有支付回调、支付失败、多次退款、退成负数、应实收不一致等等。
- 业务逻辑保证不重复退款,例如保证取消交易只会有一次,这种业务逻辑上的校验,保证不会重复退。
高并发下如何保证库存扣减
分为两种,一种是直接对放在MySQL的库存操作,一种是将库存放在Redis缓存中,异步操作完成库存扣减。
有下面这几种方案:
- 加分布式的锁,例如Redis分布式锁,再对库存进行扣减。
- 直接数据库操作,乐观锁的方式,例如:update table set count = 1 where count>0;这样的方式,用where语句判断
- 直接数据库操作,加一个悲观锁,例如数据库本身的写锁, select * for update。
- 高并发的场景下,可以将库存放在Redis缓存中,对redis操作,后续对数据库的操作可以异步进行,这里需要考虑到缓存一致性、缓存的击穿、Redis操作的原子性等等。
缓存的一致性的话,我们会有一系列的缓存一致性的操作:
- 写操作时,先使缓存无效,更新数据库,再使缓存无效;读操作时,如果有缓存,直接读,无缓存时,读数据库,并更新缓存。
- 写操作时,先更新数据库,再更新缓存;读操作,直接读取。
缓存的击穿的话,需要采用降级和限流的策略:
- 降级:如果缓存获取不到值,直接返回空,抛出异常,不要访问数据库。
- 限流:对数据库的访问进行限流,使用令牌桶的策略等等。
- 加锁:当获取不到缓存,需要查数据库并更新缓存时,需要加分布式的锁,再做操作,防止并发访问。
Redis操作的原子性:
- 单个操作就用Redis的操作命令。
- Lua脚本,多个操作可以通过Lua脚本来实现。Lua脚本中的多个命令会作为一个整体执行,不会被其他命令打断。
- Redis事务,需要注意Redis事务并不是严格意义上的原子性,如果在EXEC执行前发生错误(如语法错误),整个事务会回滚;但如果是在EXEC执行期间发生错误(如类型错误),只有出错的命令会失败,其他命令仍会执行。
- 加分布式锁之后再做操作。
分库分表的问题以及处理策略
- 数据分布不均,数据量增大时会有扩展性的问题;可以通过一致性哈希算法来解决。
- 没有办法支持跨库事务,跨库事务难以保证一致性。使用分布式事务框架(如Seata)或最终一致性方案。
- 跨库查询复杂且性能差。避免跨库查询,可以将跨库查询的操作转到其他类型的数据库,如hive、es、ck等等,虽然数据可能是T+1,有延迟性,但是支持大数据查询、跨库查询。
- 分库分表后,主键可能冲突。需要采用分布式的雪花算法提供唯一id。
- 数据迁移复杂且易出错。采用双写、数据同步工具(如Canal)逐步迁移。
如何设计一个秒杀系统
秒杀系统的话,需要考虑的就是并发量太大的问题。
可以有下面的一些思路:
- 多级缓存:针对静态资源,需要做一个预加载,做成Redis缓存+本地缓存的多层缓存结构。
- 热点数据分片:对于Redis缓存,需要考虑它的qps,太多的话,需要做数据分片,或者读取从库等,分摊它的压力。
- 限流:针对高并发,可以做一个限流的策略,通过令牌桶或者漏桶,来限制机器的QPS,或者通过Redis做分布式限流。
- 降级:考虑降级策略,例如手动关闭非核心功能,如评论、报销等等。
- 异步:非核心功能做异步处理,例如推延迟Q处理退款、发送短信等等。
- 熔断:考虑熔断机制,如果系统挂了,可以返回主页。
- 防止库存超卖,可以参考上面的。
- 考虑超时未支付关单,并且回滚库存。
- 考虑防重或者幂等,防止一个客户多次、高频的请求。
- 做好监控,例如下单成功率等等。
- 提前做好压力测试,测试极端场景的熔断、降级、限流是否生效。
支付网关两次支付如何保证一致性
使用两阶段提交的思路:通过一个协调者(事务管理器)协调两个外部系统的操作。
- 准备阶段:协调者询问两个外部系统是否准备好扣款。
- 提交阶段:如果两个外部系统都准备好,协调者通知它们提交扣款;否则,回滚。
具体实现,可以使用分布式事务,使用seata的TCC模式,在try阶段进行 金额冻结,confirm阶段进行确认扣款,cancel阶段进行资源释放:例如解冻。
费用类数据如何保证数据一致性
- 高可靠的事件通知:操作通知可以采用QMQ的方式,保证通知不会丢失。
- 数据同步策略:通过监控定期比对两边数据差异。
- 操作的原子性:可以使用分布式事务来保证一次操作的原子性,例如一次退多个退款单。没有分布式事务的话,可以用数据库的状态字段保证,通过考虑极端场景的处理来保证幂等性、可靠性。
- 通过幂等、可重试等操作,保证最终一致性:可以通过补偿来保持数据一致,支持幂等和重试。
设计一个关注和粉丝的系统,要求可以查看关注列表和粉丝列表
这时候我们是需要用到MySQL来存储关联关系的数据的。
有两种思路:
- 建立两张表,一张关注表(uid,subscriberUid),一张粉丝表(uid,funUid)。
- 建立一张关联关系表 (uid,subscriberUid),就记录关联关系就好了。
假如数据量太大了怎么办?
需要做分库分表,例如用uid当键,去做分库分表。
MySQL数据库的表数据量过大,导致查询缓慢,怎么优化
- 加索引:如果表上索引不多,可以根据分辨度高的字段加索引,例如订单号、用户id等
- 分库分表:可以通过分库分表来降低单张表的数据量
- 增加缓存:对于频繁的查询可以增加redis缓存或者本地缓存,多级缓存的策略来优化查询速度
- 读写分离:写主库,读从库,读写分离降低读的压力。
- 过时数据迁移:定期将超过1年的数据迁移到备份库里。
- 大数据表查询:非实时的查询,可以用HIVE、CK等更适合报表查询的数据库查询。