Redis学习日记(四)
Redis实战篇优惠劵秒杀1.全局唯一ID每个店铺都可以发布优惠券当用户抢购时就会生成订单并保存到tb_voucher_order这张表中而订单表如果使用数据库自增ID就存在一些问题1id的规律性太明显2受单表数据量的限制1场景分析场景分析一如果我们的id具有太明显的规则用户或者说商业对手很容易猜测出来我们的一些敏感信息比如商城在一天时间内卖出了多少单这明显不合适。场景分析二随着我们商城规模越来越大mysql的单表的容量不宜超过500W数据量过大之后我们要进行拆库拆表但拆分表了之后他们从逻辑上讲他们是同一张表所以他们的id是不能一样的 于是乎我们需要保证id的唯一性。2全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具一般要满足下列特性为了增加ID的安全性我们可以不直接使用Redis自增的数值而是拼接一些其它信息ID的组成部分符号位1bit永远为0时间戳31bit以秒为单位可以使用69年序列号32bit秒内的计数器支持每秒产生2^32个不同ID2.Redis实现全局唯一Id1代码实现Component public class RedisIdWorker { /** * 开始时间戳 */ private static final long BEGIN_TIMESTAMP 1640995200L; /** * 序列号的位数 */ private static final int COUNT_BITS 32; ​ private StringRedisTemplate stringRedisTemplate; ​ public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } ​ public long nextId(String keyPrefix) { // 1.生成时间戳 LocalDateTime now LocalDateTime.now(); long nowSecond now.toEpochSecond(ZoneOffset.UTC); long timestamp nowSecond - BEGIN_TIMESTAMP; ​ // 2.生成序列号 // 2.1.获取当前日期精确到天 String date now.format(DateTimeFormatter.ofPattern(yyyy:MM:dd)); // 2.2.自增长 long count stringRedisTemplate.opsForValue().increment(icr: keyPrefix : date); ​ // 3.拼接并返回 return timestamp COUNT_BITS | count; } }测试类知识小贴士关于countdownlatchcountdownlatch名为信号枪主要的作用是同步协调在多线程的等待于唤醒问题我们如果没有CountDownLatch 那么由于程序是异步的当异步程序没有执行完时主线程就已经执行完了然后我们期望的是分线程全部走完之后主线程再走所以我们此时需要使用到CountDownLatchCountDownLatch 中有两个最重要的方法1、countDown2、awaitawait 方法 是阻塞方法我们担心分线程没有执行完时main线程就先执行所以使用await可以让main线程阻塞那么什么时候main线程不再阻塞呢当CountDownLatch 内部维护的 变量变为0时就不再阻塞直接放行那么什么时候CountDownLatch 维护的变量变为0 呢我们只需要调用一次countDown 内部变量就减少1我们让分线程和变量绑定 执行完一个分线程就减少一个变量当分线程全部走完CountDownLatch 维护的变量就是0此时await就不再阻塞统计出来的时间也就是所有分线程执行完后的时间。Test void testIdWorker() throws InterruptedException { CountDownLatch latch new CountDownLatch(300); ​ Runnable task () - { for (int i 0; i 100; i) { long id redisIdWorker.nextId(order); System.out.println(id id); } latch.countDown(); }; long begin System.currentTimeMillis(); for (int i 0; i 300; i) { es.submit(task); } latch.await(); long end System.currentTimeMillis(); System.out.println(time (end - begin)); }3.添加优惠卷每个店铺都可以发布优惠券分为平价券和特价券。平价券可以任意购买而特价券需要秒杀抢购tb_voucher优惠券的基本信息优惠金额、使用规则等 tb_seckill_voucher优惠券的库存、开始抢购时间结束抢购时间。特价优惠券才需要填写这些信息平价卷由于优惠力度并不是很大所以是可以任意领取而代金券由于优惠力度大所以像第二种卷就得限制数量从表结构上也能看出特价卷除了具有优惠卷的基本信息以外还具有库存抢购时间结束时间等等字段新增普通卷代码VoucherControllerPostMapping public Result addVoucher(RequestBody Voucher voucher) { voucherService.save(voucher); return Result.ok(voucher.getId()); }新增秒杀卷代码VoucherControllerPostMapping(seckill) public Result addSeckillVoucher(RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); }VoucherServiceImplOverride 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(SECKILL_STOCK_KEY voucher.getId(), voucher.getStock().toString()); }4.实现秒杀下单下单核心思路当我们点击抢购时会触发右侧的请求我们只需要编写对应的controller即可秒杀下单应该思考的内容下单时需要判断两点秒杀是否开始或结束如果尚未开始或已经结束则无法下单库存是否充足不足则无法下单下单核心逻辑分析当用户开始进行下单我们应当去查询优惠卷信息查询到优惠卷信息判断是否满足秒杀条件比如时间是否充足如果时间充足则进一步判断库存是否足够如果两者都满足则扣减库存创建订单然后返回订单id如果有一个条件不满足则直接结束。VoucherOrderServiceImplOverride public 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.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 6.2.用户id Long userId UserHolder.getUser().getId(); voucherOrder.setUserId(userId); // 6.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ return Result.ok(orderId); ​ }5.库存超卖问题分析有关超卖问题分析在我们原有代码中是这么写的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(库存不足); }假设线程1过来查询库存判断出来库存大于1正准备去扣减库存但是还没有来得及去扣减此时线程2过来线程2也去查询存发现这个数量一定也大于1那么这两个线程都会去扣减库存最终多个线程相当于一起去扣减库存此时就会出现库存的超卖问题。超卖问题是典型的多线程安全问题针对这一问题的常见解决方案就是加锁而对于加锁我们通常有两种解决方案见下图悲观锁悲观锁可以实现对于数据的串行化执行比如syn和lock都是悲观锁的代表同时悲观锁中又可以再细分为公平锁非公平锁可重入锁等等乐观锁乐观锁会有一个版本号每次操作数据会对版本号1再提交回数据时会去校验是否比之前的版本大1 如果大1 则进行操作成功这套机制的核心逻辑在于如果在操作过程中版本号只比原来大1 那么就意味着操作过程中没有人对他进行过修改他的操作就是安全的如果不大1则数据被修改过当然乐观锁还有一些变种的处理方式比如cas乐观锁的典型代表就是cas利用cas进行无锁化机制加锁var5 是操作前读取的内存值while中的var1var2 是预估值如果预估值 内存值则代表中间没有被人修改过此时就将新值去替换 内存值其中do while 是为了在操作失败时再次进行自旋操作即把之前的逻辑再操作一次。int var5; do { var5 this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 var4)); ​ return var5;课程中的使用方式课程中的使用方式是没有像cas一样带自旋的操作也没有对version的版本号1 他的操作逻辑是在操作时对版本号进行1 操作然后要求version 如果是1 的情况下才能操作那么第一个线程在操作后数据库中的version变成了2但是他自己满足version1 所以没有问题此时线程2执行线程2 最后也需要加上条件version 1 但是现在由于线程1已经操作过了所以线程2操作时就不满足version1 的条件了所以线程2无法执行成功。6.乐观锁解决超卖问题修改代码方案一、VoucherOrderServiceImpl 在扣减库存时改为boolean success seckillVoucherService.update() .setSql(stock stock -1) //set stock stock -1 .eq(voucher_id, voucherId).eq(stock,voucher.getStock()).update(); //where id and stock ?以上逻辑的核心含义是只要我扣减库存时的库存和之前我查询到的库存是一样的就意味着没有人在中间修改过库存那么此时就是安全的但是以上这种方式通过测试发现会有很多失败的情况失败的原因在于在使用乐观锁过程中假设100个线程同时都拿到了100的库存然后大家一起去进行扣减但是100个人中只有1个人能扣减成功其他的人在处理时他们在扣减时库存已经被修改过了所以此时其他线程都会失败。修改代码方案二、之前的方式要修改前后都保持一致但是这样我们分析过成功的概率太低所以我们的乐观锁需要变一下改成stock大于0 即可boolean success seckillVoucherService.update() .setSql(stock stock -1) .eq(voucher_id, voucherId).update().gt(stock,0); //where id ? and stock 0知识小扩展针对cas中的自旋压力过大我们可以使用Longaddr这个类去解决Java8 提供的一个对AtomicLong改进后的一个类LongAdder大量线程并发更新一个原子性的时候天然的问题就是自旋会导致并发性问题当然这也比我们直接使用syn来的好所以利用这么一个类LongAdder来进行优化如果获取某个值则会对cell和base的值进行递增最后返回一个完整的值7.优惠券秒杀-一人一单需求修改秒杀业务要求同一个优惠券一个用户只能下一单现在的问题在于优惠卷是为了引流但是目前的情况是一个人可以无限制的抢这个优惠卷所以我们应当增加一层逻辑让一个用户只能下一个单而不是让一个用户下多个单具体操作逻辑如下比如时间是否充足如果时间充足则进一步判断库存是否足够然后再根据优惠卷id和用户id查询是否已经下过这个订单如果下过这个订单则不再下单否则进行下单VoucherOrderServiceImpl初步代码增加一人一单逻辑​ Override public 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.一人一单逻辑 // 5.1.用户id Long userId UserHolder.getUser().getId(); 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) .eq(voucher_id, voucherId).update(); if (!success) { //扣减库存 return Result.fail(库存不足); } //7.创建订单 VoucherOrder voucherOrder new VoucherOrder(); // 7.1.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); ​ voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ return Result.ok(orderId); ​ }存在问题现在的问题还是和之前一样并发过来查询数据库都不存在订单所以我们还是需要加锁但是乐观锁比较适合更新数据而现在是插入数据所以我们需要使用悲观锁操作注意在这里提到了非常多的问题我们需要慢慢的来思考首先我们的初始方案是封装了一个createVoucherOrder方法同时为了确保他线程安全在方法上添加了一把synchronized 锁Transactional public synchronized Result createVoucherOrder(Long voucherId) { ​ 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.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 7.2.用户id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ // 7.返回订单id return Result.ok(orderId); }但是这样添加锁锁的粒度太粗了在使用锁过程中控制锁粒度是一个非常重要的事情因为如果锁的粒度太大会导致每个线程进来都会锁住所以我们需要去控制锁的粒度以下这段代码需要修改为 intern() 这个方法是从常量池中拿到数据如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象new出来的对象我们使用锁必须保证锁必须是同一把所以我们需要使用intern()方法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.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 7.2.用户id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ // 7.返回订单id return Result.ok(orderId); } }但是以上代码还是存在问题问题的原因在于当前方法被spring的事务控制如果你在方法内部加锁可能会导致当前方法事务还没有提交但是锁已经释放也会导致问题所以我们选择将当前方法整体包裹起来确保事务不会出现问题如下在seckillVoucher 方法中添加以下逻辑这样就能保证事务的特性同时也控制了锁的粒度但是以上做法依然有问题因为你调用的方法其实是this.的方式调用的事务想要生效还得利用代理来生效所以这个地方我们需要获得原始的事务对象 来操作事务8.集群环境下的并发问题通过加锁可以解决在单机情况下的一人一单安全问题但是在集群模式下就不行了。1、我们将服务启动两份端口分别为8081和80822、然后修改nginx的conf目录下的nginx.conf文件配置反向代理和负载均衡有关锁失效原因分析由于现在我们部署了多个tomcat每个tomcat都有一个属于自己的jvm那么假设在服务器A的tomcat内部有两个线程这两个线程由于使用的是同一份代码那么他们的锁对象是同一个是可以实现互斥的但是如果现在是服务器B的tomcat内部又有两个线程但是他们的锁对象写的虽然和服务器A一样但是锁对象却不是同一个所以线程3和线程4可以实现互斥但是却无法和线程1和线程2实现互斥这就是 集群环境下syn锁失效的原因在这种情况下我们就需要使用分布式锁来解决这个问题。

相关新闻

降AI工具避坑指南:新手必知的5大消费陷阱

降AI工具避坑指南:新手必知的5大消费陷阱

1. 为什么你需要这份降AI工具避坑指南?第一次接触降AI工具的新手,往往会在各种隐性消费和功能陷阱上栽跟头。我见过太多人因为不了解行业潜规则,在订阅服务、插件购买和算力消耗上白白浪费几百块。就拿最常见的文字转视频工具来说&#xff0c…

2026/8/6 3:24:21 阅读更多 →
从零构建个人网页:HTML5基础、CSS布局与JavaScript交互实战

从零构建个人网页:HTML5基础、CSS布局与JavaScript交互实战

1. 项目概述:为什么从HTML个人网页开始?如果你对编程感兴趣,或者想在网上有一个属于自己的小天地,但又觉得那些复杂的框架和工具让人望而却步,那么从制作一个简单的HTML个人网页开始,绝对是条正确的路。我叫…

2026/8/6 3:24:20 阅读更多 →
logcat 清空日志全套命令

logcat 清空日志全套命令

logcat 清空日志全套命令1、基础清空(普通机型,清除 main/system/crash 缓冲区)adb logcat -c等价 adb logcat --clear。作用:清空系统默认三组日志缓冲区,执行完立刻退出,无输出。2、彻底清空全部缓冲区&a…

2026/8/6 3:23:20 阅读更多 →

最新新闻

技术突破:ComfyUI-Manager实现企业级AI绘画工作流自动化管理

技术突破:ComfyUI-Manager实现企业级AI绘画工作流自动化管理

技术突破:ComfyUI-Manager实现企业级AI绘画工作流自动化管理 【免费下载链接】ComfyUI-Manager ComfyUI-Manager is an extension designed to enhance the usability of ComfyUI. It offers management functions to install, remove, disable, and enable various…

2026/8/6 18:05:46 阅读更多 →
D2DX暗黑破坏神2高清补丁:3步实现60fps宽屏的终极指南

D2DX暗黑破坏神2高清补丁:3步实现60fps宽屏的终极指南

D2DX暗黑破坏神2高清补丁:3步实现60fps宽屏的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 还在忍受暗…

2026/8/6 18:05:46 阅读更多 →
Web Security Academy 第六关: Oracle 数据库 SQL 注入实战详解

Web Security Academy 第六关: Oracle 数据库 SQL 注入实战详解

前言SQL 联合查询注入是渗透测试中最常见的注入手段之一,但不同数据库的语法、系统视图存在明显区别,不能一套 Payload 通吃所有环境。上一篇文章我们完成了 PostgreSQL 靶场的注入实操,本篇带来同系列 Oracle 数据库专项靶场,场景…

2026/8/6 18:05:46 阅读更多 →
浏览器音乐解锁终极指南:如何免费恢复被加密的音乐文件所有权

浏览器音乐解锁终极指南:如何免费恢复被加密的音乐文件所有权

浏览器音乐解锁终极指南:如何免费恢复被加密的音乐文件所有权 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库: 1. https://github.com/unlock-music/unlock-music ;2. https://git.unlock-music.dev/um/web 项目地址…

2026/8/6 18:05:46 阅读更多 →
别只看最终答案:多模态 RAG 需要一套文档解析评测包

别只看最终答案:多模态 RAG 需要一套文档解析评测包

多模态 RAG、Agent 和 MCP 正把文档解析推到更靠前的位置:PDF、Office、扫描件、表格、公式和图表不再只是“转成文本”,而是进入知识库前的关键数据工程。近期 OmniDocBench 增加 EvalScope 集成,RAG-Anything 继续强化多模态管线&#xff0…

2026/8/6 18:05:46 阅读更多 →
5分钟上手draw.io桌面版:完全免费的跨平台图表工具实战指南

5分钟上手draw.io桌面版:完全免费的跨平台图表工具实战指南

5分钟上手draw.io桌面版:完全免费的跨平台图表工具实战指南 【免费下载链接】drawio-desktop Official electron build of draw.io 项目地址: https://gitcode.com/GitHub_Trending/dr/drawio-desktop 还在为寻找一款既专业又免费的图表软件而烦恼吗&#xf…

2026/8/6 18:04:46 阅读更多 →

日新闻

深入解析LimboAI C++内核:架构设计与性能优化实战

深入解析LimboAI C++内核:架构设计与性能优化实战

1. 项目概述:为什么我们需要深入LimboAI的C内核?如果你是一名使用Godot引擎的游戏开发者,尤其是对AI行为逻辑有较高要求的项目,那么LimboAI这个名字你大概率不会陌生。它作为Godot 4生态中一个备受瞩目的行为树与状态机插件&#…

2026/8/6 0:00:06 阅读更多 →
Unity 2D游戏敌人AI系统:基于PlayMaker状态机与2D Toolkit的实战开发

Unity 2D游戏敌人AI系统:基于PlayMaker状态机与2D Toolkit的实战开发

1. 项目概述与核心思路大家好,我是老张,一个在游戏开发一线摸爬滚打了十多年的老码农。今天咱们接着聊《空洞骑士》风格2D动作游戏的Demo制作。上一期我们搭好了基础框架,处理了角色移动和碰撞,这一期,我们要让游戏世界…

2026/8/6 0:00:06 阅读更多 →
被动防火门市场前景发展趋势

被动防火门市场前景发展趋势

被动防火门依靠材质结构、密闭构造阻隔烟火蔓延,无需电控启动,是建筑被动消防系统核心构件,行业依托新规管控、城市更新、工业安全升级迎来稳定扩容,整体朝着合规化、专项化、低碳化、智能化方向发展。现阶段 GB12955‑2024 新版国…

2026/8/6 0:00:06 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/5 15:00:43 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/5 13:13:56 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/5 10:20:36 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/5 23:28:39 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/5 21:00:14 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/5 23:46:51 阅读更多 →