Redis从入门到精通(六)Redis实战(三)优惠券秒杀

↑↑↑下载测试项目原代码↑↑↑

文章目录

    • 前言
    • 4.3 优惠券秒杀
      • 4.3.1 数据表与实体类
      • 4.3.2 添加优惠券
        • 4.3.2.1 添加普通券代码
        • 4.3.2.2 添加秒杀券代码
      • 4.3.3 实现秒杀下单
        • 4.3.3.1 秒杀下单逻辑分析
        • 4.3.3.2 获取秒杀订单ID
        • 4.3.3.3 获取用户ID
        • 4.3.3.4 实现秒杀下单

前言

Redis实战系列文章:

Redis从入门到精通(四)Redis实战(一)短信登录
Redis从入门到精通(四)Redis实战(二)商户查询缓存

4.3 优惠券秒杀

每个商户都可以发布优惠券,分为普通券和秒杀券。普通券优惠力度较低,所以可以任意购买;而秒杀券优惠力度较大,需要限时限量抢购。例如:

4.3.1 数据表与实体类

在数据库中,tb_voucher表用于保存优惠券的信息,包括优惠券的基本信息、优惠金额、使用规则等:

tb_seckill_voucher表用于保存秒杀券的扩展信息,包括秒杀券的库存、开始抢购时间、结束抢购时间等:

tb_voucher_order表用于保存优惠券购买订单信息,包括购买用户的ID、优惠券的ID、购买时间等:

根据以上三个数据表在项目中创建三个对应的实体类,如下:

// tb_voucher、tb_seckill_voucher
// com.star.redis.dzdp.pojo.Voucher/**** 优惠券* @author hsgx* @since 2024/4/5 10:26*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_voucher")
public class Voucher implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 商铺id*/private Long shopId;/*** 代金券标题*/private String title;/*** 副标题*/private String subTitle;/*** 使用规则*/private String rules;/*** 支付金额*/private Long payValue;/*** 抵扣金额*/private Long actualValue;/*** 优惠券类型*/private Integer type;/*** 优惠券类型*/private Integer status;/*** 库存*/@TableField(exist = false)private Integer stock;/*** 生效时间*/@TableField(exist = false)private Date beginTime;/*** 失效时间*/@TableField(exist = false)private Date endTime;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;}
// tb_seckill_voucher
// com.star.redis.dzdp.pojo.SeckillVoucher/**** 秒杀优惠券表* @author hsgx* @since 2024/4/5 10:26*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_seckill_voucher")
public class SeckillVoucher implements Serializable {private static final long serialVersionUID = 1L;/*** 关联的优惠券的id*/@TableId(value = "voucher_id", type = IdType.INPUT)private Long voucherId;/*** 库存*/private Integer stock;/*** 创建时间*/private Date createTime;/*** 生效时间*/private Date beginTime;/*** 失效时间*/private Date endTime;/*** 更新时间*/private Date updateTime;}
// tb_voucher_order
// com.star.redis.dzdp.pojo.VoucherOrder/**** 优惠券订单* @author hsgx* @since 2024/4/5 10:30*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_voucher_order")
public class VoucherOrder implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.INPUT)private Long id;/*** 下单的用户id*/private Long userId;/*** 购买的代金券id*/private Long voucherId;/*** 支付方式 1:余额支付;2:支付宝;3:微信*/private Integer payType;/*** 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款*/private Integer status;/*** 下单时间*/private Date createTime;/*** 支付时间*/private Date payTime;/*** 核销时间*/private Date useTime;/*** 退款时间*/private Date refundTime;/*** 更新时间*/private Date updateTime;}

4.3.2 添加优惠券

4.3.2.1 添加普通券代码
  • 1)接口文档
项目说明
请求方法POST
请求路径/voucher/add
请求参数Voucher
返回值
  • 2)代码实现

首先为普通优惠券业务创建对应的VoucherController类-IVoucherService接口-VoucherServiceImpl实现类-VoucherMapper类;为秒杀优惠券业务创建对应的ISeckillVoucherService接口-SeckillVoucherServiceImpl实现类-SeckillVoucherMapper类。详见测试项目代码。

然后在VoucherController类中编写一个add()方法,用于新增普通优惠券:

// com.star.redis.dzdp.controller.VoucherController@Resource
private IVoucherService voucherService;/*** 添加普通优惠券* @author hsgx* @since 2024/4/5 10:43* @param voucher* @return com.star.redis.dzdp.pojo.BaseResult*/
@PostMapping("/add")
public BaseResult add(@RequestBody Voucher voucher) {log.info("add {}", voucher.toString());voucherService.save(voucher);log.info("add success. id = {}", voucher.getId());return BaseResult.setOk("添加普通优惠券成功");
}
  • 3)功能测试

编写完成后,调用/voucher/add接口,新增一个普通优惠券:

控制台打印信息如下:

add Voucher(id=null, shopId=1, title=500元代金券, subTitle=周一至周日均可使用, rules=全场通用 无需预约 可无限叠加 不兑现、不找零 仅限堂食, payValue=45000, actualValue=50000, type=null, status=null, stock=null, beginTime=null, endTime=null, createTime=null, updateTime=null)
==>  Preparing: INSERT INTO tb_voucher ( shop_id, title, sub_title, rules, pay_value, actual_value ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 1(Long), 500元代金券(String), 周一至周日均可使用(String), 全场通用 无需预约 可无限叠加 不兑现、不找零 仅限堂食(String), 45000(Long), 50000(Long)
<==    Updates: 1
add success. id = 10
4.3.2.2 添加秒杀券代码
  • 1)接口文档
项目说明
请求方法POST
请求路径/voucher/seckill/add
请求参数Voucher
返回值
  • 2)代码实现

在VoucherController类中编写一个addSeckill()方法,调用IVoucherService接口的addSeckillVoucher()方法,用于新增秒杀优惠券:

// com.star.redis.dzdp.controller.VoucherController/*** 添加秒杀优惠券* @author hsgx* @since 2024/4/5 11:00* @param voucher* @return com.star.redis.dzdp.pojo.BaseResult*/
@PostMapping("/seckill/add")
public BaseResult addSeckill(@RequestBody Voucher voucher) {return voucherService.addSeckillVoucher(voucher);
}
// com.star.redis.dzdp.service.impl.VoucherServiceImpl@Resource
private ISeckillVoucherService seckillVoucherService;@Resource
private StringRedisTemplate stringRedisTemplate;@Override
public BaseResult addSeckillVoucher(Voucher voucher) {log.info("add a seckill voucher, {}", voucher.toString());// 1.保存优惠券信息save(voucher);log.info("add voucher success. id = {}", voucher.getId());// 2.保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 3.将秒杀优惠券的库存保存到RedisString key = "seckill:stock:" + voucher.getId();stringRedisTemplate.opsForValue().set(key, voucher.getStock().toString());log.info("set to Redis : Key = {}, Value = {}", key, voucher.getStock().toString());return BaseResult.setOk("新增秒杀券成功!");
}
  • 3)功能测试

编写完成后,调用/voucher/seckill/add接口,新增一个秒杀优惠券:

控制台打印信息如下:

add a seckill voucher, Voucher(id=null, shopId=1, title=1000元代金券, subTitle=限时秒杀, rules=周一至周日均可使用, payValue=50000, actualValue=100000, type=null, status=null, stock=1000, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Fri Apr 05 18:00:00 CST 2024, createTime=null, updateTime=null)
==>  Preparing: INSERT INTO tb_voucher ( shop_id, title, sub_title, rules, pay_value, actual_value ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 1(Long), 1000元代金券(String), 限时秒杀(String), 周一至周日均可使用(String), 50000(Long), 100000(Long)
<==    Updates: 1
add voucher success. id = 11
==>  Preparing: INSERT INTO tb_seckill_voucher ( voucher_id, stock, begin_time, end_time ) VALUES ( ?, ?, ?, ? )
==> Parameters: 11(Long), 1000(Integer), 2024-04-05 
<==    Updates: 1
set to Redis : Key = seckill:stock:11, Value = 1000

此时,在数据库的tb_voucher表有2条记录,tb_seckill_voucher有1条记录:

4.3.3 实现秒杀下单

4.3.3.1 秒杀下单逻辑分析

如上图所示,当用户下单时,会提交优惠券的ID,后台根据该ID查询对应的优惠券信息,并判断秒杀是否开始,如果未开始,则直接返回错误信息;如果已经开始,则再次判断库存是否充足,如果不充足,则直接返回错误信息;如果充足,则扣减库存,并创建订单,并返回订单ID。

4.3.3.2 获取秒杀订单ID

每个商户都可以发布订单ID,并且保存到tb_voucher_order表中,如果这个表的ID使用自增ID,则是存在一些问题的:ID的规律太明显,且受到单表数据量的限制。

如果ID具有太明显的规律,用户或者商业对手就很容易猜测出商户的一些敏感信息,比如商户在一天时间内卖出了多少单,这明显不合适。

同时,随着商户规模的扩大,需要存入数据库的数据量也在变大,而MySQL的单表容量不宜超过500W。数据量过大之后,**就要进行拆库拆表,拆分之后,从逻辑上讲它们仍然是同一张表,所以ID是也不能是一样的。

综上,需要保证订单ID的唯一性。本项目使用全局ID生成器来解决这个问题。 全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具,满足唯一性、高可用、高性能、递增型和安全性等。

为了增加ID的安全性,使用以下编码方式生成全局唯一ID:

  • 符号位:永远是0
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,支持每秒产生2^32个不同的ID

下面就创建一个工具类RedisIdWorker,用于生成全局唯一ID:

// com.star.redis.dzdp.utils.RedisIdWorkerpublic class RedisIdWorker {/*** 获取全局唯一ID* @author hsgx* @since 2024/4/5 12:37* @param stringRedisTemplate* @param keyPrefix* @return long*/public static long nextId(StringRedisTemplate stringRedisTemplate, String keyPrefix) {// 1.生成时间戳long nowSec = System.currentTimeMillis() / 1000;// 2.生成序列号long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + new Date());// 3.拼接并返回return nowSec << 32 | count;}
}
4.3.3.3 获取用户ID

在tb_voucher_order表中,有一个user_id字段,该字段保存了下单用户的ID。下单用户即当前登录的用户,因此要想办法在业务层拿到当前登录用户的ID。

我们在登录拦截器中已经拿到了当前登录用户的信息,因此可以将这里拿到的信息继续向下游传递:

// com.star.redis.dzdp.interceptor.LoginInterceptor#preHandle()// 5.存在,放行
// 将用户ID向下游传递
request.setAttribute("userId", user.getId());
return true;

如此,后续在Controller方法中,就可以使用request.getAttribute("userId")方法获取用户ID。

4.3.3.4 实现秒杀下单

为优惠券订单业务创建对应的IVoucherOrderService接口-VoucherOrderServiceImpl实现类-VoucherOrderMapper类。详见测试项目代码。

在IVoucherOrderService接口中定义一个seckillVoucher()方法,并在VoucherOrderServiceImpl实现类中具体实现,作用是秒杀下单。

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private IVoucherOrderService voucherOrderService;
@Resource
private StringRedisTemplate stringRedisTemplate;@Override
public BaseResult<Long> seckillVoucher(Long voucherId, Long userId) {log.info("开始秒杀下单...voucherId = {}, userId = {}", voucherId, userId);// 1.查询秒杀优惠券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀活动是否开启或结束if(seckillVoucher == null) {// 秒杀活动不存在return BaseResult.setFail("秒杀活动不存在!");} else if(seckillVoucher.getBeginTime().after(new Date())) {// 秒杀活动未开始log.info("beginTime = {}", seckillVoucher.getBeginTime());return BaseResult.setFail("秒杀尚未开始!");} else if(seckillVoucher.getEndTime().before(new Date())) {// 秒杀活动已结束log.info("endTime = {}", seckillVoucher.getEndTime());return BaseResult.setFail("秒杀尚已结束!");}log.info("{}", seckillVoucher.toString());// 3.判断库存是否充足if(seckillVoucher.getStock() < 1) {// 库存不足return BaseResult.setFail("库存不足,抢券失败!");}// 4.扣减库存boolean update = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();log.info("update result = {}", update);if(!update) {// 扣减库存失败,返回抢券失败return BaseResult.setFail("库存不足,抢券失败!");}// 5.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 5.1 设置订单IDLong orderId = RedisIdWorker.nextId(stringRedisTemplate, "voucher_order");log.info("get orderId = {}", orderId);voucherOrder.setId(orderId);// 5.2 设置用户IDvoucherOrder.setUserId(userId);// 5.3 设置订单其他信息voucherOrder.setVoucherId(voucherId);voucherOrder.setPayTime(new Date());voucherOrderService.save(voucherOrder);// 6 返回订单IDreturn BaseResult.setOkWithData(orderId);
}

接着在VoucherOrderMapper类中编写一个seckillOrder()方法,作为秒杀下单的接口。

  • 1)接口文档
项目说明
请求方法POST
请求路径/voucher/seckill/order
请求参数Voucher
返回值订单ID
  • 2)代码实现
/*** 秒杀下单* @author hsgx* @since 2024/4/5 12:41* @param voucherOrder* @return com.star.redis.dzdp.pojo.BaseResult*/
@PostMapping("/seckill/order")
public BaseResult seckillOrder(@RequestBody VoucherOrder voucherOrder, HttpServletRequest request) {return voucherOrderService.seckillVoucher(voucherOrder.getVoucherId(), (Long)request.getAttribute("userId"));
}
  • 3)功能测试

编写完成后,调用/voucher/seckill/order接口,新增一个秒杀订单:

控制台打印信息如下:

开始秒杀下单...voucherId = 11, userId = 1012
==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
==> Parameters: 11(Long)
<==      Total: 1
SeckillVoucher(voucherId=11, stock=999, createTime=Fri Apr 05 11:25:00 CST 2024, beginTime=Fri Apr 05 06:00:00 CST 2024, endTime=Fri Apr 05 18:00:00 CST 2024, updateTime=Fri Apr 05 12:56:15 CST 2024)
==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ?)
==> Parameters: 11(Long)
<==    Updates: 1
update result = true
get orderId = 7354246043942256641
==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
==> Parameters: 7354246043942256641(Long), 1012(Long), 11(Long), 2024-04-05 13:10:40.332(Timestamp)
<==    Updates: 1

至此,秒杀下单完成。

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/302153.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

diffusion model(十五) : IP-Adapter技术小结

infopaperhttps://arxiv.org/pdf/2308.06721.pdfcodehttps://github.com/tencent-ailab/IP-Adapterorg.Tencent AI Lab个人博客地址http://myhz0606.com/article/ip_adapter 1 Motivation 为了对文生图diffusion model进行特定概念的定制&#xff0c;常用LoRA[1]、textual in…

JDK下载及安装说明

1&#xff0e;JDK下载 访问oracle官网&#xff1a;http://www.oracle.com 在首页点击Downloads&#xff0c;进入oracle软件下载页。 在下载页面&#xff0c;点击Java。 选择Java (JDK) for Developers&#xff0c;点击。 在 Java SE Downloads 页面&#xff0c;点击中间的DO…

装机指导。

everything winrar snipaste cmake git tortoisegit tortoisesvn inno setup vs2022 安装的时候注意sdk路径一定要默认&#xff01;&#xff01; 否则你会发现在你的sdk安装路径的根盘符下会多出一个Windows Kits&#xff0c;强迫症接受不了 默认的会跟已有的装在一起…

【Python系列】读取 Excel 第一列数据并赋值到指定列

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Linux的学习之路:4、权限

一、Linux权限的概念 权限我们都熟悉&#xff0c;最常见的就是在看电视时需要vip这个就是权限&#xff0c;然后在Linux就是有两个权限&#xff0c;就是管理员也就是超级用户和普通的用户 命令&#xff1a;su [用户名] 功能&#xff1a;切换用户。 例如&#xff0c;要从root用户…

ZLMediaKit ubantu 下编译

1、获取代码 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init二、依赖库 Debian系(包括ubuntu&#xff09;系统下安装依赖的方法&#xff1a; #除了…

Python-VBA函数之旅-ascii函数

ascii函数在Python中主要用于将对象(特别是字符和字符串)转换为它们的ASCII表示形式。这种转换在处理文本数据、调试代码以及确保文本以 ASCII 格式存储或传输时非常有用。常见应用场景有&#xff1a; 1、调试和文本处理&#xff1a;当处理包含非ASCII字符(如Unicode字符)的文…

景联文科技:为AI大模型提供高质海量训练数据

在全球AI浪潮的推动下&#xff0c;大量训练数据已成为AI算法模型发展和演进中的关键一环。 艾瑞咨询数据显示&#xff0c;包括数据采集、数据处理&#xff08;标注&#xff09;、数据存储、数据挖掘等模块在内的AI基础数据服务市场&#xff0c;将在未来数年内持续增长。 预计到…

算法:完全背包问题dp

文章目录 一、完全背包问题的特征二、定义状态三、状态转移四、降维优化五、参考例题5.1、Acwing&#xff1a;3.完全背包问题5.2、Acwing&#xff1a;900. 整数划分 一、完全背包问题的特征 完全背包问题是动态规划中的一种经典问题&#xff0c;它的主要特征可以总结如下&…

ES6中 Promise的详细讲解

文章目录 一、介绍状态特点流程 二、用法实例方法then()catchfinally() 构造函数方法all()race()allSettled()resolve()reject() 三、使用场景# 参考文献 一、介绍 Promise&#xff0c;译为承诺&#xff0c;是异步编程的一种解决方案&#xff0c;比传统的解决方案&#xff08;…

2024/4/5—力扣—在排序数组中查找元素的第一个和最后一个位置

代码实现&#xff1a; 思路&#xff1a;二分法 方法一&#xff1a;分别查找左右侧边界 /*** Note: The returned array must be malloced, assume caller calls free().*/ int GetTargetFirstPosition(int *nums, int numsSize, int target) {int l 0, r numsSize - 1;while …

springboot无人便利店信息管理系统ssm+tomcat+java

jdk版本&#xff1a;1.8 及以上 ide工具&#xff1a;IDEA 或者eclipse 数据库: mysql 编程语言: java 框架&#xff1a;SSM/springboot都有 maven: 3.6.1 前端&#xff1a;layuibootstrapjsp 详细技术&#xff1a;HTMLCSSJSjspspringmvcmybatisMYSQLMAVENtomcat本文以java实现…

Jenkins使用-绑定域控与用户授权

一、Jenkins安装完成后&#xff0c;企业中使用&#xff0c;首先需要绑定域控以方便管理。 操作方法&#xff1a; 1、备份配置文件&#xff0c;防止域控绑定错误或授权策略选择不对&#xff0c;造成没办法登录&#xff0c;或登录后没有权限操作。 [roottest jenkins]# mkdir ba…

iOS 开发中上传 IPA 文件的方法(无需 Mac 电脑

引言 在 iOS 开发中&#xff0c;将 IPA 文件上传到苹果开发者中心是一个重要的步骤。通常情况下&#xff0c;我们需要使用 Mac 电脑上的 Xcode 或 Application Loader 工具来完成这个任务。然而&#xff0c;如果你没有 Mac 电脑&#xff0c;也没有关系&#xff0c;本文将介绍一…

Windows编译运行yolov9-bytetrack-tensorrt (C++)

Windows编译运行yolov9-bytetrack-tensorrt&#xff08;C&#xff09; 1 基础环境2 编译yolov9-bytetrack-tensorrt&#xff08;1&#xff09;下载yolov9-bytetrack-tensorrt源码&#xff08;2&#xff09;修改CMakeLists.txt&#xff08;3&#xff09;CMake编译 3 yolov9模型转…

css实现各级标题自动编号

本文在博客同步发布&#xff0c;您也可以在这里看到最新的文章 Markdown编辑器大多不会提供分级标题的自动编号功能&#xff0c;但我们可以通过简单的css样式设置实现。 本文介绍了使用css实现各级标题自动编号的方法&#xff0c;本方法同样适用于typora编辑器和wordpress主题…

有没有适合运动佩戴的耳机?最适合运动使用的开放式耳机推荐

哪种耳机更适合运动&#xff0c;挂耳式和入耳式哪种更合适呢&#xff1f;答案是挂耳式的耳机更适合运动&#xff0c;适用的场景也更多。无论你是在家还是在外面运动&#xff0c;都很合适。挂耳式耳机也可以叫开放式耳机&#xff0c;它开放式的设计可以让我们更好的感知到周围嘈…

1132A安捷伦1132A示波器探头

181/2461/8938产品概述&#xff1a; 带宽: 输入阻抗: 差分输入R: 50千欧差分输入C: 0.27-0.34 pF单端输入电阻:25千欧单端输入C: 0.44-0.67 pF 连通性: E2669A差分/单端连接套件E2668A单端连接套件用于InfiniiMax探头的E2675A差分浏览器套件E2677A InfiniiMax 12 GHz差分焊…

APx500音频分析仪硬件简介

两通道模拟输出&#xff0c;两通道或以上的模拟输入接口 线性编码数字音频接口&#xff08;AES/EBU,TOSLINK,SPDIF&#xff09;Linear PCM 脉冲密度调制码流&#xff08;需要APx-PDM选件支持&#xff09; Bluetooth蓝牙音频码流&#xff08;需APx-BT选件支持&#xff09; 最…

DataGrip 2024 for Mac/Win—数据库管理的得力助手

在当今的数据驱动世界中&#xff0c;高效地管理数据库至关重要。无论您是数据库管理员、开发人员还是数据分析师&#xff0c;DataGrip 2024 都是您不可或缺的工具。 DataGrip 2024 适用于 Mac 和 Win 系统&#xff0c;具有以下卓越特性&#xff1a; 全面支持多种数据库&#…