分布式锁(仅供自己参考)

分布式锁:满足分布式系统或集群式下多进程可见并且互斥的锁(使用外部的锁,因为如果是集群部署,每台服务器都有一个对应的tomcat,则每个tomcat的jvm就不同,锁对象就不同(加锁的机制,每个jvm有一个锁监听器,里面存放着是否含有所有加锁的对象。有一次线程来,首先判断该线程所具有的所要加锁的对象在锁监视器中是否有,有则让它等待,没有的话则将加锁的对象加进来,再有线程来判断,则和上面的流程一样))(自己理解的流程,可能不对,请指错)

redis实现分布式锁:

一些主要代码:

因为在加锁的时候new的不同的对象,使得每一个name都是不同的。

如果是在高并发的情况下,会有一点的影响(具体的影响是问的chatgpt的)     

        在高并发情况下,如果每个线程都频繁创建和销毁大量对象,这会对Java堆内存产生一定的影响,主要体现在以下几个方面:

  1. 内存使用压力增大:

    • 大量的对象实例占用堆内存空间,可能会导致堆内存使用率急剧上升。
    • 如果不及时回收无用对象,会造成内存溢出(OutOfMemoryError)的风险。
  2. 垃圾回收压力增大:

    • 大量短暂对象的频繁创建和销毁,会增加Java虚拟机的垃圾回收压力。
    • 垃圾回收频率提高,可能会导致应用的性能下降。
  3. 线程间资源竞争:

    • 多个线程同时在堆内存中分配和释放对象,可能会产生线程安全问题。
    • 如果对象的创建和销毁不够规范,可能会导致内存泄漏等问题。
  4. 缓存命中率下降:

    • 大量短暂对象的创建可能会污染CPU缓存,降低缓存命中率。
    • 这会增加内存访问的开销,拖慢整体应用的性能。

可以采用单例模式,也就是在将锁对象作为成员变量,而不是局部变量。(代码参考(参考这里1))

package com.hmdp.utils;

import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;

//redis实现的锁机制
public class OrderOneLock implements Lock {
    private RedisTemplate redisTemplate;

    private String name;
    private final String PREFIX_KEY = "lock:";

    public OrderOneLock(RedisTemplate redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeOut) {

        long id = Thread.currentThread().getId();

        Boolean b = redisTemplate.opsForValue().setIfAbsent(PREFIX_KEY + name, id, Duration.ofSeconds(timeOut));
        return Boolean.TRUE.equals(b);
    }

    @Override
    public void unLock() {

        redisTemplate.delete(PREFIX_KEY+name);
    }
}





//加锁
 Long userId = UserHolder.getUser().getId();

        //获取锁,看是否获取成功

        OrderOneLock oneLock = new OrderOneLock(redisTemplate,"order:"+userId); //选择userId是因为每个用户只能选择一单,如果没有userid则全部都使用一单
        boolean b = oneLock.tryLock(120);
        if (!b)
        {
            return Result.fail("只能购买一单");
        }

        //能执行到这里,说明加锁成功,则之后必须不管怎么都要释放锁,所以选择finally
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.getResult(voucherId, userId);
        } finally {
            oneLock.unLock();
        }

参考这里(1)

因为此代码较上边的代码,是为了避免在高并发的情况的大量的创建和销毁对象,而造成性能上的影响。但也是这段代码毕竟麻烦,在于是在销毁锁的时候需要传参数,毕竟有舍有得(具体业务具体分析)。

代码:

package com.hmdp.utils;

import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;

public class OrderOneLock implements Lock {
    private RedisTemplate redisTemplate;

    //private String name;
    private final String PREFIX_KEY = "lock:";
    private String workName; //具体的业务名字

    public OrderOneLock(RedisTemplate redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.workName = name;
    }

    @Override
    public boolean tryLock(long timeOut,String name) {

        long id = Thread.currentThread().getId();

        Boolean b = redisTemplate.opsForValue().setIfAbsent(PREFIX_KEY + workName+name, id, Duration.ofSeconds(timeOut));
        return Boolean.TRUE.equals(b);
    }

    @Override
    public void unLock(String name) {

        redisTemplate.delete(PREFIX_KEY+workName+name);
    }
}

上述代码还会造成一种情况,请看下图:

        由于线程1的阻塞,导致redis的key的过期。这时候key过期,线程2就可以拿到锁了,如果这时候线程1,业务完成结束,肯定是需要释放锁的,但这个时候释放的是线程2的锁,而不是线程1它本身的锁。线程2的锁释放之后,但是线程2的业务还未完成,线程3也就来了,导致了有两个线程同时运行,于我们加锁(只能由一个线程执行相违背),而且前一个线程会释放后一个线程的锁。

解决方案:

        给每一个线程的锁都弄一个标识,使得每一个线程都只能由它本身释放,或者过期(这里还没有解决会有多个线程同时执行)

        使用uuid和当前线程号给每一个线程一个标识。uuid在同一个jvm可能相同,但是在不同的jvm之间不同(每一个jvm运行在不同的机器或者虚拟机上)。原因:

  1. UUID生成算法: 在Java中,UUID通常使用RFC 4122中定义的算法生成。这个算法利用了时间戳、MAC地址、随机数等多种因素来生成唯一的UUID。

  2. 时间因素: 算法中使用了时间戳作为生成因素之一。不同的JVM,即使在同一时间生成,由于机器时钟的微小差异,也会导致生成的UUID不同。

  3. 空间因素: UUID的算法还会利用MAC地址作为生成因素。不同的JVM运行在不同的服务器/虚拟机上,MAC地址必不同,这也会导致生成的UUID不同。

           在加上同一个jvm上的线程号是不同的,也就构成了唯一一个标识(jvm使得线程号是递增的)

代码:(自己比较与上面代码的差别)

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.RedisTemplate;

import java.awt.*;
import java.time.Duration;

public class OrderOneLock implements Lock {
    private RedisTemplate redisTemplate;

    private String name;
    private final String PREFIX_KEY = "lock:";
    private final String mark = UUID.randomUUID().toString(true)+"-";

    public OrderOneLock(RedisTemplate redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeOut) {

        long id = Thread.currentThread().getId();

        Boolean b = redisTemplate.opsForValue().setIfAbsent(PREFIX_KEY +name, mark+id, Duration.ofSeconds(timeOut));
        return Boolean.TRUE.equals(b);
    }

    @Override
    public void unLock() {
        long id = Thread.currentThread().getId();
        String o = (String) redisTemplate.opsForValue().get(PREFIX_KEY + name);
        String s = mark + id;
        if(s.equals(o))
        {
            redisTemplate.delete(PREFIX_KEY+name);
        }
    }
}

新的情况出现:

如上图:在线程1执行业务完成之后,需要释放锁,在已判断了他是属于他的锁,就要执行 redisTemplate.delete(PREFIX_KEY+name);这句时发生了阻塞(jvm的垃圾回收机制,导致所有代码都不能运行)。一直阻塞。阻塞时间超过了key的过期时间。锁释放。等到阻塞结束,这时又会有线程2进来,线程1也是执行 redisTemplate.delete(PREFIX_KEY+name);这句。这时候线程1就会释放线程2的锁。释放之后就会有线程3进来,这时就会有两个线程执行这个业务(发生了并发),不符合我们的要求。

解决方案:

使得判断和删除为一个操作,使得操作为原子操作。这要就要使用外部的脚本代码(lua)

lua代码:

if(redis.call('get',KEYS[1]) == ARGV[1]) then
    return redis.call('del',KEYS[1])
end

return 0;

Java代码:

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.awt.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class OrderOneLock implements Lock {
    private RedisTemplate redisTemplate;

    private String name;
    private final static String PREFIX_KEY = "lock:";
    private final static String mark = UUID.randomUUID().toString(true)+"-"; //为啥是static:
    // 因为每个jvm都会执行这段代码,每个jvm就会生成唯一一个标识。并且每个jvm的线程不可能相同
    private final static DefaultRedisScript<Long> SCRIPT_UNLOCK;    //首先静态提前加载

    static {
        SCRIPT_UNLOCK = new DefaultRedisScript<>();
        SCRIPT_UNLOCK.setLocation(new ClassPathResource("unLock.lua"));
        SCRIPT_UNLOCK.setResultType(Long.class);
    }


    public OrderOneLock(RedisTemplate redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeOut) {

        long id = Thread.currentThread().getId();

        Boolean b = redisTemplate.opsForValue().setIfAbsent(PREFIX_KEY +name, mark+id, Duration.ofSeconds(timeOut));
        return Boolean.TRUE.equals(b);
    }

    @Override
    public void unLock() {
        long id = Thread.currentThread().getId();
        String s = mark+id;
        redisTemplate.execute(SCRIPT_UNLOCK, Collections.singletonList(PREFIX_KEY+name),s);
    }

    //@Override
    //public void unLock() {
    //    long id = Thread.currentThread().getId();
    //    String o = (String) redisTemplate.opsForValue().get(PREFIX_KEY + name);
    //    String s = mark + id;
    //    if(s.equals(o))
    //    {
    //        redisTemplate.delete(PREFIX_KEY+name);
    //    }
    //}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值