Redis分布式锁-秒杀类锁不住及各种因为锁导致的“血案”现场全侦破代码详解

news2024/10/11 22:29:04

背景

继上文“详解Redis分布式锁在SpringBoot的@Async方法中没锁住的坑”不少读者发觉用了我的方法还是在并发的情况下有锁不住!

于是我和几个没有锁住的读者了解了它们的场景,才知道他们在认知上存在几个误区,同时也发现这一块内容、知识真的在网上普及的不多,很多人都是看了2016年左右用jedis的分布式锁套到了redisson上写的例子,100%都是错的。

特别当再有并发场景情况下,代码排查就更难了,如果没有安装商业级的APM或者是JProfile根本抓不到在并发(300-500并发甚至是上千,上万并发)情况下到底JVM内部发生了什么事,导致出了问题难排查,痛苦不堪。

因此,特再出此一文,用代码+jmeter并发(300并发/sec)彻底讲清redis锁的各种用法。

本文适合场景

我们在此文中会使用一个最最复杂的场景,这个场景在零售中叫“秒杀”,包括零售中还有一种就是“订单盖帽”即今天我就只有1,000单卖完了1,000单再下的订单全部返回“已售馨”这叫订单盖帽正是此场景。

用这个场景来讲redis分布式锁的正确使用姿势再恰当不过了。

锁的基本用法在上文“详解Redis分布式锁在SpringBoot的@Async方法中没锁住的坑”中已经讲了。有不少读者回去试了,也用了并发测试,结果发觉还是没有锁住。

好!

我们来看为什么!

为什么用我上篇文章中的代码没有锁住的详细分析

首先我这边要说一句,我给出的场景是一个锁的基本用法,而读者们用的并发是对着spring boot的controller层做并发测试。

这个场景是不适合我上一篇文章中的代码的。

这是因为以下几个因素:

  1. 我上一篇的代码是告诉了大家锁的基本用法,它属于一个很简单的业务场景,同时通过演示正反示例告诉了大家锁要锁的范围和锁的状态在并发时会产生乱序,我们把它统称为“竞态”。
  2. 它适合于单controller层+异步线程Service,而且这个单controller在设计时就不会发生controller并发的问题,这是因为我的这个controller在实际我的生产级代码中有一个AOP,这个AOP使用的是另一种机制来控制住了此controller不会也不可能产生并发;

这部分代码涉及到我们自己代码的商业机密,因此不便于展示,但是我仔细想了一下,可能就是因为没有把这一层说透,所以我用另一个真实零售中的秒杀(包括订单盖帽)的逻辑来讲读者就更清楚了。

通过代码来理解Controller层也并发的情况下,锁的机制

这边需要说一个核心,当controller层不可能(代码手段上防止它会产生并发)产生并发时+@Async的Service时,按照我上一篇做一点问题没有。

那么这次我们把我们的Service层去掉@Async,此时因为Service层已经不会有并发中的并发乱序了,所以我们此时可以把锁的全状态判断放到@Service方法里了。

下面来看代码

controller代码

 @RequestMapping(value = "/demo/service/takeCoupon", method = RequestMethod.POST)
    @ResponseBody
    public ResponseBean takeCoupon(@RequestBody JSONObject params) {
        ResponseBean resp = new ResponseBean();
        try {
            CouponBean couponBean = this.promotionService.takeCoupon();
            return new ResponseBean(ResponseCodeEnum.SUCCESS.getCode(), "success", couponBean);
        } catch (Exception e) {
            logger.error(">>>>>>Promotion API接口访问错误->{}", e.getMessage(), e);
            resp = new ResponseBean(ResponseCodeEnum.FAIL.getCode(), "Promotion API接口访问错误", null);
        }
        return resp;
    }

service代码

public CouponBean takeCoupon() {
        CouponBean couponBean = new CouponBean();
        RLock lock = redissonSentinel.getLock(lockName);
        try {
            if (!lock.isLocked()) {
                lock.tryLock(0, TimeUnit.SECONDS);// 上锁
                couponBean = new CouponBean();
                couponBean.setResult(0);
                couponBean.setMsg("抢券中");
                redisTemplate.opsForValue().set(redisName, couponBean);
                Thread.sleep(5000);//模拟一个业务动作需要5秒
                String couponId = RandomNumUtil.getUniqueSequence(8);
                couponBean.setResult(1);
                couponBean.setMsg("抢券成功");
                couponBean.setCouponId(couponId);
                redisTemplate.opsForValue().set(redisName, couponBean);
            } else {
                couponBean = new CouponBean();
                couponBean.setResult(2);
                couponBean.setMsg("前方抢券排队中");
                redisTemplate.opsForValue().set(redisName, couponBean);
            }
        } catch (Exception e) {
            logger.error(">>>>>>抢券服务方法发生了严重的问题->{}", e.getMessage(), e);
            couponBean = new CouponBean();
            couponBean.setResult(-1);
            couponBean.setMsg("系统错误");
            redisTemplate.opsForValue().set(redisName, couponBean);
        } finally {
            try {
                lock.unlock();//释放锁,一定外部要加try和空catch()
            } catch (Exception e) {
            }
        }
        return couponBean;
    }

API调用

然后我们通过这样的API调用

curl --location --request POST 'http://localhost:9180/demo/service/takeCoupon' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: localhost:9180' \
--header 'Connection: keep-alive' \
--data-raw '{
    
}'

得到结果是对的。

于是我们手工多发几次,每次请求都是5秒后再触发。

于是我们用Jmeter

直接用Jmeter测试Controller(此时理念已经错误了)

如截图,我们模拟了100个并发,在1秒内同时发起请求。

成功可以进入锁的我们的返回状态为1否则都为错误,这是我们在Jmeter里对每个请求埋了一个json断言

100个请求并发下去得到了以下结果,成功了4次,有4个并发成功进入Service的最终锁方法。

什么都不修改的情况下,清除jmeter记录,再来一次,这一轮只有1个并发成功进入了抢券。

错误的理念导致了场景设计错误最后搞混了测试和开发自己

看到以下结果,一堆人开始挠头皮了,唉呀,这都锁住了呀,怎么一会4个成功、一会又变成了1个成功,多测几次还出现过21次成功、33次成功。

好,有了问题这才是好事,这才是寻求真理的正确道路。

以上的结果我先告诉大家,对也不对!

哈哈哈,看到这,不要急。

测试场景设计错误了

这是因为在上述这个代码的情况下,你用并发去测controller是想要测什么呢?你看一下这个jmeter,你觉得你心里的期望得到的正确结果是个什么样的值呢?

后台5秒处理一个请求,我在1秒内一口气发过去了100个,可是为什么这个值一会4、一会2、一会21、一会33?难道不应该是1个?

没有搞明白系统的并发

这边看清了哦,你要对controller进行并发锁的测试,这是一种“限流式”的场景,5秒处理一个后台你可以加上时间戳代码,肯定保证5秒一个并发,至于具体会多少次,嘿嘿,这取决于你系统当前的状态。下面关键的知识点来了:

  • 系统有时一口气一秒吃20个并发,那么就可以多处理一些(因为并行的处理,我在一瞬间可能已经处理掉50个请求了,隔5秒再处理剩下的请求)。
  • 系统有时开着音乐、看着视频、下着迅雷,那么此时处理能力就弱一些(因为并行的处理,我在一瞬间只可能处理19个,隔5秒再处理剩下的请求)。

而jmeter的并发线程池,只给了1秒种就中断请求了,因此此时后台返回的这个请求可以成功进入到锁的返回值就是那1秒内被处理掉的请求数量!

所以,这样的并发测试后说:还是没锁住?这。。。你到底锁什么、测什么呢?你的观察数据、指标又是什么?

因此如果我们用上述这样的场景来观察并发锁是否锁住本身就失去了测锁的意义!

那么怎么测锁才是真正正确的做法?

其实在我上一篇就如我所叙,我的Controller保证不会并发,并发的是我的Service,所以在那个场景,我只要在Service里起一个Thread Pool就可以马上发觉我的Service里的锁有没有锁对并保证全局只有一个任务。

而现在由于大多测试和开发的日常习惯性思维我们一定要从Controller即API层用Jmeter去测并发,所以呢,我们就需要重新设计一个场景了。

秒杀(包括订单盖帽、抢券)场景

这个场景就是秒杀场景,这是因为如果要测controller并发我们需要有一个“指标”,这个指标就是“消耗的量、指标可预判以及可观察”,所以让我们先来看业务逻辑。

业务逻辑

  1. 全局只有2单(包括2张券);
  2. 300个人每人点5次抢券按钮;
  3. 无论是谁得到券,全局券(订单数)始终不得超过2;

为了方便观察我们设计全局数量即这个TOTAL_COUPON_NUMS就只有2;

上代码

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final static String lockName = "com.xmall.promotion.coupon.lock";
    private final static String redisName = "com.xmall.promotion.coupon.status";
    private final static String couponAmountRedisName = "com.xmall.promotion.coupon.amount";
    private final static int TOTAL_COUPON_NUMS = 2;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonSentinel;

反例

Controller层代码
    @RequestMapping(value = "/demo/service/secKillWrong", method = RequestMethod.POST)
    @ResponseBody
    public ResponseBean secKillWrong(@RequestBody JSONObject params) {
        ResponseBean resp = new ResponseBean();
        try {
            CouponBean couponBean = this.promotionService.secKillWrong();
            return new ResponseBean(ResponseCodeEnum.SUCCESS.getCode(), "success", couponBean);
        } catch (Exception e) {
            logger.error(">>>>>>Promotion API接口访问错误->{}", e.getMessage(), e);
            resp = new ResponseBean(ResponseCodeEnum.FAIL.getCode(), "Promotion API接口访问错误", null);
        }
        return resp;
    }
Service层代码
public CouponBean secKillWrong() {
        CouponBean couponBean = new CouponBean();
        int takedCouponNums = 0;
        try {

            couponBean = new CouponBean();
            couponBean.setResult(0);
            couponBean.setMsg("抢券中");

            /*判断券数量*/
            Object obj = redisTemplate.opsForValue().get(couponAmountRedisName);
            if (obj != null) {
                    takedCouponNums = (int) obj;
                    logger.info(">>>>>>takedCouponNums->{}",takedCouponNums);
                    if(takedCouponNums<TOTAL_COUPON_NUMS){
                        redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));
                        couponBean = new CouponBean();
                        couponBean = this.getCoupon();
                    }
                    else {
                        couponBean = new CouponBean();
                        couponBean.setResult(3);
                        couponBean.setMsg("你来晚了,券被抢完了");
                        return couponBean;
                    }
            }
        } catch (Exception e) {
            logger.error(">>>>>>抢券服务方法发生了严重的问题->{}", e.getMessage(), e);
            couponBean = new CouponBean();
            couponBean.setResult(-1);
            couponBean.setMsg("系统错误");
            redisTemplate.opsForValue().set(redisName, couponBean);
        }
        return couponBean;
    }

    private CouponBean getCoupon() throws Exception {
        CouponBean couponBean = new CouponBean();
        Thread.sleep(1000);
        couponBean = new CouponBean();
        couponBean.setResult(1);
        couponBean.setMsg("抢成功");
        return couponBean;
    }
Jmeter开测

我们设置好json断言,每个API返回中如果data.result=1那么就是抢券成功,从理论上来说我们甚至用到了redis的原子递增“redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));”,而且在递增前我们还做了当前redis内的TOTAL值是否已经达到2的判断,那么就一定可以避免多个并发的情况下递增超过2。

 实际结果

哈哈哈哈,完全错了!

我还看到过一次竟然抢成功了333次!

而系统全局只有2张券(包括订单)啊?

分析问题

这就是多并发情况下的“竞态”导至的问题。实际真正在后台jvm内部是这样的一种情况。

同时会存在1秒内多个并发一下涌入,此时全部在以下这条判断时得到的是<2

                if (obj != null) {
                    takedCouponNums = (int) obj;
                    logger.info(">>>>>>takedCouponNums->{}",takedCouponNums);
                    if(takedCouponNums<TOTAL_COUPON_NUMS){
                        redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));
                        couponBean = new CouponBean();
                        couponBean = this.getCoupon();
                    }
                    else {
                        couponBean = new CouponBean();
                        couponBean.setResult(3);
                        couponBean.setMsg("你来晚了,券被抢完了");
                        return couponBean;
                    }
                }

有多少条一下同时得到了<2的值就代表有多少条请求会去执行redis的increment方法。那么成功抢到券的就有多少个人!

如何解决?

此时就得上Redis分布式锁了,要把上述这个判断逻辑给它锁住,那么就对了。

自以为对的又一反例

什么叫自以为对呢?

因为这也是我们的小朋友在实际开发中没有理解透Redis锁导致的“血案”,而且错的相当经典,因此我在此一并贡献出来给到所有人通过这些例子真正理解透“redis分布式锁“。

secKill Service方法
public CouponBean secKill() {
        CouponBean couponBean = new CouponBean();
        RLock lock = redissonSentinel.getLock(lockName);
        int takedCouponNums = 0;
        try {
            if (!lock.isLocked()) {
                lock.tryLock(0, TimeUnit.SECONDS);// 上锁
                couponBean = new CouponBean();
                couponBean.setResult(0);
                couponBean.setMsg("抢券中");

                /*判断券数量*/
                Object obj = redisTemplate.opsForValue().get(couponAmountRedisName);
                if (obj != null) {
                    takedCouponNums = (int) obj;
                    logger.info(">>>>>>takedCouponNums->{}",takedCouponNums);
                    if(takedCouponNums<TOTAL_COUPON_NUMS){
                        redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));
                        couponBean = new CouponBean();
                        couponBean = this.getCoupon();
                    }
                    else {
                        couponBean = new CouponBean();
                        couponBean.setResult(3);
                        couponBean.setMsg("你来晚了,券被抢完了");
                        return couponBean;
                    }
                }
            } else {
                couponBean = new CouponBean();
                couponBean.setResult(2);
                couponBean.setMsg("前方抢券排队中");
                redisTemplate.opsForValue().set(redisName, couponBean);
            }
        } catch (Exception e) {
            logger.error(">>>>>>抢券服务方法发生了严重的问题->{}", e.getMessage(), e);
            couponBean = new CouponBean();
            couponBean.setResult(-1);
            couponBean.setMsg("系统错误");
            redisTemplate.opsForValue().set(redisName, couponBean);
        } finally {
            try {
                lock.unlock();//释放锁,一定外部要加try和空catch()
            } catch (Exception e) {
            }
        }
        return couponBean;
    }
getCoupon方法
    private CouponBean getCoupon() throws Exception {
        CouponBean couponBean = new CouponBean();
        Thread.sleep(1000);
        redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));
        couponBean = new CouponBean();
        couponBean.setResult(1);
        couponBean.setMsg("抢成功");
        return couponBean;
    }
开测

我们用同样的jmeter参数,300个并发1秒内发起,跑5轮。

结果如下:

看到这我已经不要看下去了,直接中断jmeter端测试了,这是因为我一共只有2张券,success超过2(即data.result的返回值1)就不对了。

错在哪?

嘿嘿,我们的小朋友还挺好,做了一个getCoupon的方法

    private CouponBean getCoupon() throws Exception {
        CouponBean couponBean = new CouponBean();
        Thread.sleep(1000);
        redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(1));
        couponBean = new CouponBean();
        couponBean.setResult(1);
        couponBean.setMsg("抢成功");
        return couponBean;
    }

我们来看这个方法内那一行:redis的increment方法。

自以为在主service方法secKill里这样调用

               if (obj != null) {
                    takedCouponNums = (int) obj;
                    logger.info(">>>>>>takedCouponNums->{}",takedCouponNums);
                    if (takedCouponNums>= TOTAL_COUPON_NUMS) {
                        couponBean = new CouponBean();
                        couponBean.setResult(3);
                        couponBean.setMsg("你来晚了,券被抢完了");
                        return couponBean;
                    } else{                        
                        couponBean = new CouponBean();
                        couponBean = this.getCoupon();
                    }
                }

就能锁住?

这里面涉及到了一个“业务原子性”的终极解释!

业务原子性的真正奥义
第1点对Redis锁错误上的认知

所谓业务原子性,即“同一个方法内”,而不是跨方法。虽然getCoupon()方法被套在了secKill方法内调用,但它还是跨方法了。

因此呢,当redis increment方法被调用时它“超脱”了redis分布式锁的控制了,因此就导致了这样的情况发生:

在一秒内可能有超过30-50个并发(取决于系统的吐吞能力)同时increment了一下并且没有跟随在if判断是否当前的消耗数量<TOTAL_COUPON_NUMS的后面。

第2点对Redis锁错误上的认知

我们通过先去redis取值,判断,如果满足<TOTAL_COUPON_NUMS那么就会做redis的increment。

于是判断<TOTAL_COUPON_NUMS和increment不在一个“原子”操作里,这是因为increment本身就是一个原子操作,如果要把判断<TOTAL_COUPON_NUMS和increment纳入一个原子操作你需要这么干:

Long takedCouponNums = redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(0));
if (takedCouponNums >= TOTAL_COUPON_NUMS) {
   couponBean.setResult(3);
   couponBean.setMsg("你来晚了,券被抢完了");
} else {
   // 增加优惠券数量
   redisTemplate.opsForValue().increment(couponAmountRedisName, 1);
   // 获取优惠券
   couponBean = this.getCoupon();
}
第3点对Redis锁错误上的认知
 RLock lock = redissonSentinel.getLock(lockName);
        int takedCouponNums = 0;
        try {
            if (!lock.isLocked()) {
                lock.tryLock(0, TimeUnit.SECONDS);// 上锁

上述这段代码是我在上一篇例子中即controller没有并发,Service层因为是@Async的有并发的情况下才能这么用。

而现在Controller层有并发了,此时如果再这么写就会产生“锁跳逸”,即锁的状态因为父层是并发的于是产生了状态混乱导致部分锁状态在一瞬间可能“!isLocked",进而导致期望值出错,我们需要这样写:

 RLock lock = redissonSentinel.getLock(lockName);
        try {
            if (lock.tryLock(0, TimeUnit.SECONDS)) {

以上3点就是为什么上了“锁”还是错了的原因所在。

如何解决-真正的正例

知道了“业务原子性”的终极奥义后,我们动手来改代码:

public CouponBean secKill() {
        CouponBean couponBean = new CouponBean();
        RLock lock = redissonSentinel.getLock(lockName);
        try {
            if (lock.tryLock(0, TimeUnit.SECONDS)) {
                couponBean = new CouponBean();
                couponBean.setResult(0);
                couponBean.setMsg("抢券中");
                /*判断券数量*/
                Long takedCouponNums = redisTemplate.opsForValue().increment(couponAmountRedisName, Long.valueOf(0));
                if (takedCouponNums >= TOTAL_COUPON_NUMS) {
                    couponBean.setResult(3);
                    couponBean.setMsg("你来晚了,券被抢完了");
                } else {
                    // 增加优惠券数量
                    redisTemplate.opsForValue().increment(couponAmountRedisName, 1);
                    // 模拟获取优惠券如取券号、把券号和用户的ID绑在一起等业务操作
                    couponBean = this.getCoupon();
                }
            } else {
                couponBean = new CouponBean();
                couponBean.setResult(2);
                couponBean.setMsg("前方抢券排队中");
                redisTemplate.opsForValue().set(redisName, couponBean);
            }
        } catch (Exception e) {
            logger.error(">>>>>>抢券服务方法发生了严重的问题->{}", e.getMessage(), e);
            couponBean = new CouponBean();
            couponBean.setResult(-1);
            couponBean.setMsg("系统错误");
            redisTemplate.opsForValue().set(redisName, couponBean);
        } finally {
            try {
                lock.unlock();//释放锁,一定外部要加try和空catch()
            } catch (Exception e) {
            }
        }
        return couponBean;
    }

用并发测试结果来检验我们的理论是否正确

此外,并发测试除了测性能测这种并发时数据是否对,一轮是不够的,至少要跑10轮才能说明并发场景下数据没问题(这是因为并发环境存在很大的随机性),于是我们开跑jmeter。

第一轮

 

再看redis里的值

 结果正确。

第二轮

跑前先把redis里的值改成0再跑,否则就是100%错误了即0个success。

结果正确。

第三轮

我们来个狠一点的,300个并发跑10轮。

嘿!怎么可能再错呢? 始终都是对的,这就是因为:

  1. 我们在后台用了分布式锁,锁住了判断券、订单总数以及消耗量计算这个逻辑。
  2. 同时我们的增加用的是redis的increment(原子增)。
  3. 我们还把一系列操作正确的纳入了到了原子操作。
  4. 同时我们还避免了上层方法是并发的情况下的“锁跳逸”问题。

因此整个过程都是“排它”的,因此随便你跑几轮或者多少个并发(只要系统撑得住http请求),现在这个数据总量是永远不会再错了!

好了!

到此为止,结束今天的博客。此处希望读者按照教程一个个代码+jmeter自己动动手去验证一下,才能真正的彻底的领会redis锁的精妙之处和那些个“坑”。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker】06-DockerCompose

1. Docker compose 2. Docker Compose部署项目 docker-compose.yml version: "3.8"services:mysql:image: mysqlcontainer_name: mysqlports:- "3307:3306"environment:TZ: Asia/ShanghaiMYSQL_ROOT_PASSWORD: 123volumes:- "/root/docker/mysql/…

【GESP】C++一级练习BCQM3037,简单计算,国庆七天乐收官

又回到了简单计算的题目&#xff0c;继续巩固练习。 题解详见&#xff1a;https://www.coderli.com/gesp-1-bcqm3037/ 【GESP】C一级练习BCQM3037&#xff0c;简单计算&#xff0c;国庆七天乐收官 | OneCoder又回到了简单计算的题目&#xff0c;继续巩固练习。https://www.cod…

Windows 部署 ollama

1.安装 官方地址&#xff1a;https://ollama.com/ 1.1 下载ollama 1.2 安装 1.3 运行 Ollama 1.4 测试ollama运行状态 http://localhost:11434 curl http://localhost:11434/api/generate -d "{ \"model\": \"qwen2:0.5b\", \"prompt\":…

深入理解 Java HashMap 的 get() 方法及其相关实现

在 Java 中&#xff0c;HashMap 是一个非常常用的数据结构&#xff0c;用于存储键值对。它提供了快速的查找、插入和删除操作。HashMap 的核心功能之一是根据键获取对应的值&#xff0c;这主要通过 get() 方法来实现。本文将详细介绍 HashMap 的 get() 方法及其相关的辅助方法&…

初中数学网上考试系统的设计与实现(论文+源码)_kaic

初中数学网上考试系统的设计与实现 学生&#xff1a; 指导教师&#xff1a; 摘 要&#xff1a;科技在人类的历史长流中愈洗愈精&#xff0c;不仅包括人们日常的生活起居&#xff0c;甚至还包括了考试的变化。之前的考试需要大量的时间和精力&#xff0c;组织者还需要挑选并考查…

【大模型部署】本地运行自己的大模型--ollama

ollama简介 ollama是一款开源的、轻量级的框架&#xff0c;它可以快速在本地构建及运行大模型&#xff0c;尤其是一些目前最新开源的模型&#xff0c;如 Llama 3, Mistral, Gemma等。 官网上有大量已经开源的模型&#xff0c;部分针对性微调过的模型也可以选择到&#xff0c;…

Shell脚本linux登录自动检查

.bashrc 用于设置用户的 Bash shell 环境&#xff0c;在每次打开一个新的终端窗口或启动一个新的 Bash 会话时被执行 代码 login_check.sh #!/bin/bash clear LogFileNamepolling.$(date %F-%T) EchoFormat$(for (( i0; i<30; i )); do echo -n ""; done)# 显示…

死磕P7:JVM性能调优必知必会(二)

这是「死磕P7」系列第 008 篇文章&#xff0c;欢迎大家来跟我一起 死磕 100 天&#xff0c;争取在 2025 年来临之际&#xff0c;给自己一个交代。 接上篇&#xff0c;性能优化工具&#xff0c;尤其是图形化工具&#xff0c;绝对有 VisualVM 的一席之地&#xff0c;因为它几乎囊…

【Node.js】图片水印

上传时加水印 用户上传原始图片->服务器&#xff1a;保留原始图片以及水印图片动态水印 用户上传原始图片->服务器&#xff1a;只保留原始图片 请求图片时&#xff0c;服务器动态加水印 根据业务需求自行更改操作&#xff0c;下面只讲最简单的给图片加水印。 主要使用到…

遨游智能终端赋能“危急特”场景,力推北斗技术规模化应用!

随着《北斗规模应用三年行动计划&#xff08;2023-2025&#xff09;》的发布&#xff0c;北京、湖北、重庆等多地出台北斗支持政策&#xff0c;北斗系统正稳步迈向“安全可控&#xff0c;泛在融合&#xff0c;开放兼容&#xff0c;服务全球”的发展目标。遨游通讯紧跟国家战略步…

10/11

一、ARM课程大纲 二、ARM课程学习的目的 2.1 为了找到一个薪资水平达标的工作&#xff08;单片机岗位、驱动开发岗位&#xff09; 应用层(APP) 在用户层调用驱动层封装好的API接口&#xff0c;编写对应的API接口 ----------------------------------------------------…

怎么做接口自动化测试

在分层测试的“金字塔”模型中&#xff0c;接口测试属于第二层服务集成测试范畴。相比UI层&#xff08;主要是WEB或APP&#xff09;自动化测试而言&#xff0c;接口自动化测试收益更大&#xff0c;且容易实现&#xff0c;维护成本低&#xff0c;有着更高的投入产出比&#xff0…

2024徐州科技企业-京东(无锡)基地数字经济交流座谈会

2024年6月4日下午,2024徐州科技企业-京东(无锡)基地数字经济交流座谈会在无锡市经开区京东(无锡)数字基地成功举办,本次活动由无锡经济开发区管理委员会指导,京东科技主办,无锡经开雪浪小镇未来园区有限公司、江南大学经贸学院协办。来自徐州市的40家高新技术企业以及行业专家、…

TTM-RE: Memory-Augmented Document-Level Relation Extraction(内存增强的文档级关系提取)

摘要 文档级关系提取旨在对文档中任意两个实体之间的关联进行分类。以往的文档级关系提取方法在充分利用不同噪声水平的大量训练数据的潜力方面是无效的。例如&#xff0c;在ReDocRED基准数据集中&#xff0c;在大规模、低质量、远距离监督的训练数据上训练的最先进的方法通常…

lnmp - RBAC方案设计与实现

概述 实践的是一套企业内部使用后台OA管理系统&#xff0c;对这套系统设计的RBAC&#xff08;Role-Based Access Control&#xff0c;基于角色的访问控制&#xff09;,RBAC 方案旨在通过将后台用户与角色进行关联&#xff0c;再将角色与权限进行关联&#xff0c;实现对系统资源…

力扣之607.销售员

文章目录 1. 607.销售员1.1 题目说明1.2 准备数据1.3 解法1.4 结果截图 1. 607.销售员 1.1 题目说明 表: SalesPerson ------------------------ | Column Name | Type | ------------------------ | sales_id | int | | name | varchar | | salary | int | | commission_ra…

【CURL命令】命令行或脚本进行API测试

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 【CURL命令】命令行或脚本进行API测试 简介使用…

InfiniiVision HD3 系列示波器

_XLT_ InfiniiVision HD3 系列示波器 苏州新利通仪器仪表 使用带有定制专用集成电路 &#xff08;ASIC&#xff09; 的便携式示波器执行数字调试&#xff0c;该电路提供的垂直分辨率是其他通用示波器的四倍&#xff0c;注入噪声是其他通用示波器的一半。使用示波器进行调试&…

canvas:绘制点和点之间连线

效果图&#xff1a; <template><div class"home-box"><canvas id"canvas" /><div class"lightCircle" ref"circleRef" v-for"(item,index) in 5" :key"index"></div><div cla…

makefile与gdb的使用

✨前言✨ &#x1f4d8; 博客主页&#xff1a;to Keep博客主页 &#x1f646;欢迎关注&#xff0c;&#x1f44d;点赞&#xff0c;&#x1f4dd;留言评论 ⏳首发时间&#xff1a;2024年10月11日 &#x1f4e8; 博主码云地址&#xff1a;渣渣C &#x1f4d5;参考书籍&#xff1a…