Redis 三 Redis分布式锁

news2024/10/17 12:17:39

Redis 实战应用


文章目录

  • Redis 实战应用
  • Redis 实现全局唯一ID
  • Redis解决购物秒杀思路
    • 超卖问题
    • 一人一单
    • 集群并发的问题
      • 分布式锁
      • Redis分布式锁的实现核心思路
      • 实现分布式锁版本一
      • Redis分布式锁误删情况
      • Redis分布式锁原子性问题



Redis 实现全局唯一ID

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

在这里插入图片描述为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
使用redis实现全局唯一ID , 并且redistributio有一个increment 方案,是采用自增方案。
在这里插入图片描述
ID的组成部分:符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

@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.获取当前日期,精确到天
        // key:"icr:" + keyPrefix + ":" + date 。首先redis单个key有限制,最多2的64 次。其次使用date ,以后方便统计每天的订单数
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回   数字高位是时间戳,低位是序列号 。  所以需要将时间戳的值向左移动32位,然后使用或运算填充 count
        return timestamp << COUNT_BITS | count;
    }
}

测试类

知识小贴士:关于countdownlatch

countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题
我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch
CountDownLatch 中有两个最重要的方法
1、countDown
2、await
await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

// 定义一个 线程池
    private ExecutorService es = Executors.newFixedThreadPool(500);


@Test
void testIdWorker() throws InterruptedException {

    CountDownLatch latch = new CountDownLatch(300);  // 300个线程
    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();  // 每当一个线程执行完毕,执行一次countDown
    };
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 300; i++) { //300个线程
        es.submit(task);
    }
    latch.await(); // 等待所有countDown 结束
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}

Redis解决购物秒杀思路

超卖问题

多线程秒杀会造成库存多次错误扣减,因此需要引入锁来解决超卖问题。
在这里插入图片描述
在这里插入图片描述悲观锁:

悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等

乐观锁:

乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas

乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。
在这里插入图片描述
乐观锁解决超卖问题:


```java
boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
// .gt("stock",0)  只要库存大于0  就可以放心扣减

一人一单

一人一单即没人只能秒杀一单,如果不做限制那么会造成商家损失。单线程下,只需要通过用户查询是否已经下单了来解决,但是并发情况下,就需要使用锁来进一步限制。乐观锁适合更新数据,而插入数据适合使用悲观锁。

@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);
}

但是上述代码,是方法上加syn锁,导致锁的颗粒度很粗糙,任何线程来了都会被锁。我们可以根据 用户id 来加锁。

@Transactional
public  Result createVoucherOrder(Long voucherId) {
	Long userId = UserHolder.getUser().getId();
	synchronized(userId.toString().intern()){
	// 这里intern 是从常量池中拿到数据。
	//如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,new出来的对象,我们使用锁必须保证锁必须是同一把,所以我们需要使用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.创建订单
        // 。。。。。。
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    }
}

以上代码解决了锁的颗粒度问题,但是依旧存在新的问题,问题的原因在于以上方法被事务锁控制,如果在方法内部加锁,可能导致事务还没提交就锁已经被释放了,这样其他线程就有漏洞可以穿插。所以我们选择将当前方法整体包裹起来,确保事务不会出现问题:如下:
在这里插入图片描述
注意此时调用方法是用this来调用的,事务生效的原理是使用代理对象来生效。所以这个地方,我们需要获得原始的事务对象, 来操作事务:
在这里插入图片描述在Spring AOP中,即使你在类内部使用this来调用一个被@Transactional注解的方法,Spring的AOP代理仍然会拦截这个调用并应用事务管理。这是因为Spring的AOP代理是在运行时动态创建的,并且会代理类的所有公共方法(除非配置了特定的代理策略)。因此,如果你只是想在类内部调用一个被事务管理的方法,你可以直接使用this.methodName(),而不需要担心代理对象。Spring会确保事务被正确管理。

集群并发的问题

虽然通过加锁可以解决单机情况下的一人一单安全问题,但在集群下就不能这样了。
由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。
在这里插入图片描述

分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路。
在这里插入图片描述那么分布式锁他应该满足一些什么样的条件呢?

可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

互斥:互斥是分布式锁的最基本的条件,使得程序串行执行

高可用:程序不易崩溃,时时刻刻都保证较高的可用性

高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能

安全性:安全也是程序中必不可少的一环

在这里插入图片描述常见的分布式锁有三种

Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见

Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁

Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案
在这里插入图片描述

Redis分布式锁的实现核心思路

实现分布式锁时需要实现的两个基本方法:

获取锁:

互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败返回false
释放锁:

手动释放
超时释放:获取锁时添加一个超时时间

**核心思路:**利用redis 的setnx方法,当有多个线程进入时,我们就该利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可。
在这里插入图片描述

实现分布式锁版本一

  • 加锁逻辑
    SimpleRedisLock

利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性

private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId = Thread.currentThread().getId()
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}
  • 释放锁逻辑
    SimpleRedisLock

释放锁,防止删除别人的锁

public void unlock() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}

//创建锁对象(新增代码)
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }

Redis分布式锁误删情况

逻辑说明:

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

**解决方案:**在获取锁时存入线程标示(可以用UUID表示) 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致。
获取锁:

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

释放锁:

public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

Redis分布式锁原子性问题

** 极端情况** 线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,

在这里插入图片描述这个时候可以使用LUA脚本解决这个问题,将获取线程标识和删除锁执行到一行。
释放锁的业务流程是这样的

​ 1、获取锁中的线程标示

​ 2、判断是否与指定的标示(当前线程标示)一致

​ 3、如果一致则释放锁(删除)

​ 4、如果不一致则什么都不做

如果用Lua脚本来表示则是这样的:

最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

Java代码调用LUA脚本

public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}

总结:基于Redis的分布式锁实现思路:

利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性

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

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

相关文章

【排序】——1.冒泡排序法(含优化)

冒泡排序 1.原理 左边大于右边交换一趟排下来最大的交换到右边来(接下来所以文章用升序举例) 从左到右&#xff0c;相邻元素进行比较。 每次比较一轮&#xff0c;就会找到序列中最大的一个&#xff08;最小的一个——降序&#xff09;。这个数就会从序列的最右边冒出来。 以…

NetSarang Xshell v8.0060 Linux终端管理器个人免费版

NetSarang Xshell 官方个人完全免费中文版&#xff0c;Xshell特别版&#xff0c;Xshell 个人完全免费&#xff0c;Xshell 是一款最好用的Linux远程连接工具&#xff0c;免费SSH客户端、主机服务器远程管理客户端 。Xshell&#xff0c;轻松管理远程服务器&#xff0c;会话管理器…

16进制数据如何得到奇偶校验位??

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

详解mac系统通过brew安装mongodb与使用

本文目录 一、通过brew安装MongoDB二、mongodb使用示例1、启动数据库2、创建/删除数据库3、创建/删除集合 三、MongoDB基本概念1&#xff09;数据库 (database)2&#xff09;集合 &#xff08;collection&#xff09;3) 文档&#xff08;document&#xff09;4&#xff09;mong…

海博思创神秘股东捡漏入股,是否存利益输送?关联交易信披不一致

作者&#xff1a;Eric 来源&#xff1a;IPO魔女 今日&#xff0c;北京海博思创科技股份有限公司&#xff08;简称海博思创&#xff09;上会&#xff0c;将冲刺科创板。海博思创是储能设备生产集成商&#xff0c;处于产业链的中游&#xff0c;主要产品为电化学储能系统。 IPO魔…

护眼台灯横评:书客、柏曼、明基哪款使用体验好,又能护眼?

如果你使用过护眼台灯&#xff0c;就太能理解为什么护眼台灯会诞生了。护眼台灯确实有一定的护眼作用&#xff0c;光线柔和不刺眼&#xff0c;许多护眼台灯还有智能调光、定时休息等人性化功能。在当今这个数字化时代&#xff0c;长时间面对电脑屏幕或埋头于书本已成为许多人的…

美国的云服务器什么价格能买到?

美国的云服务器什么价格能买到&#xff1f;美国的云服务器价格月租费用一般在70元至数千元人民币不等&#xff0c;基础配置的云服务器月租金可能在20至50美元之间&#xff0c;而高端配置的云服务器月租金可能达到150至500美元。美国的云服务器价格因多个因素而异&#xff0c;包…

web前端网页用户注册页面

源码&#xff1a; <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>用户注册</title> </head> <body><form action"#" metho…

2. MySQL数据库基础

一、数据库的操作 1. 显示当前的数据库 SHOW DATABASES;2. 创建数据库 语法&#xff1a; CREATE DATABASE [IF NOT EXISTS] db_name [create_specification...];//create_specification包括&#xff1a;[DEFAULT] CHARACTER SET charset_name[DEFAULT] COLLATE collation_n…

TemporalBench:一个专注于细粒度时间理解的多模态视频理解的新基准。

2024-10-15&#xff0c;由威斯康星大学麦迪逊分校、微软研究院雷德蒙德等机构联合创建了TemporalBench&#xff0c;它通过大约10K个视频问答对&#xff0c;提供了一个独特的测试平台&#xff0c;用以评估各种时间理解和推理能力&#xff0c;如动作频率、运动幅度、事件顺序等。…

物联网的应用以及优势

物联网智能项目涵盖了多个行业领域&#xff0c;随着技术的不断进步和普及&#xff0c;越来越多的应用案例成为主流趋势。此篇文章将概述一些主要的物联网智能项目类别及其优势和日常使用场景&#xff1a; 主流物联网智能项目 1. 智能家居: •优势: 提升居住体验&#xff0c;…

倍福TwinCAT程序中遇到的bug

文章目录 问题描述&#xff1a;TwinCAT嵌入式控制器CX5140在上电启动后&#xff0c;X001网口接网线通讯灯不亮&#xff0c;软件扫描不到硬件网口 解决方法&#xff1a;硬件断电重启后&#xff0c;X001网口恢复正常 问题描述&#xff1a;TwinCAT软件点击激活配置后&#xff0c;…

CUDA 全局内存

全局内存在片外。 特点是&#xff1a;容量最大、延迟最大、使用最多 全局内存中的数据是所有线程可见的&#xff0c;Host端可见&#xff0c;且具有与程序相同的生命周期 动态全局内存 主机代码使用CUDA运行时API &#xff1a; cudaMalloc 声明内存空间&#xff1b; cudaFree…

仓储管理系统原型图移动端(WMS),出入库管理、库存盘点、库存调拨等(Axure原型、Axure实战项目)

仓储管理系统原型图移动端 Warehouse Management System Prototype 仓储管理系统原型图移动端是一个以图形化方式展示系统移动端界面和功能的原型设计图。原型图展示和说明系统移动端的功能和界面布局&#xff0c;为相关利益方提供一个直观的视觉化展示&#xff0c;帮助他们更…

《安全历史第5讲——秦始皇统一度量衡与代码标准化》

秦始皇统一了六国后&#xff0c;中国由此进入了一个崭新的历史时期。而帝国初期的一项重要决策——统一度量衡&#xff0c;让中国领先了千年&#xff0c;至今都因此受益。度量衡的统一极大地促进了社会经济交流&#xff0c;提高了行政效率&#xff0c;并为科学技术的发展奠定了…

【汇编语言】寄存器(内存访问)(五)—— 数据段

文章目录 前言1. 数据段2. 问题3. 问题的分析与解答结语 前言 &#x1f4cc; 汇编语言是很多相关课程&#xff08;如数据结构、操作系统、微机原理&#xff09;的重要基础。但仅仅从课程的角度出发就太片面了&#xff0c;其实学习汇编语言可以深入理解计算机底层工作原理&#…

Maven与Gradle的区别

Maven与Gradle是两种流行的构建工具&#xff0c;广泛用于Java项目的管理和构建。以下是它们的对比&#xff0c;包括官网、Windows 11配置环境、在IDEA中的相同点和不同点&#xff0c;以及它们各自的优缺点。 官网 Maven官网: https://maven.apache.orgGradle官网: https://gr…

idea2024启动Java项目报Error running CloudPlApplication. Command line is too long.

idea2024启动Java项目报Error running CloudPlApplication. Command line is too long. 解决方案&#xff1a; 1、打开Edit Configurations 2、点击Modify options设置&#xff0c;勾选Shorten command line 3、在Edit Configurations界面下方新增的Shorten command line选项中…

NSIS通过路径杀死指定进程解决方法

当软件安装时&#xff0c;如果有特定的软件正在运行&#xff0c;则新软件无法对其进行覆盖。同样当软件卸载时&#xff0c;如果其正在运行&#xff0c;则有残留的文件删不干净。所以便出现了在安装卸载中杀死指定进程的需求。 文章目录 一、踩坑记录1. FindProcDLL和KillProcD…

CSS3 提示框带边角popover

CSS3 提示框带边角popover。因为需要绝对定位子元素&#xff08;这里就是伪元素&#xff09;&#xff0c;所以需要将其设置为相对对位 <!DOCTYPE html> <html> <head> <title>test1.html</title> <meta name"keywords" con…