分布式锁:满足分布式系统或集群式下多进程可见并且互斥的锁(使用外部的锁,因为如果是集群部署,每台服务器都有一个对应的tomcat,则每个tomcat的jvm就不同,锁对象就不同(加锁的机制,每个jvm有一个锁监听器,里面存放着是否含有所有加锁的对象。有一次线程来,首先判断该线程所具有的所要加锁的对象在锁监视器中是否有,有则让它等待,没有的话则将加锁的对象加进来,再有线程来判断,则和上面的流程一样))(自己理解的流程,可能不对,请指错)
redis实现分布式锁:
一些主要代码:
因为在加锁的时候new的不同的对象,使得每一个name都是不同的。
如果是在高并发的情况下,会有一点的影响(具体的影响是问的chatgpt的)
在高并发情况下,如果每个线程都频繁创建和销毁大量对象,这会对Java堆内存产生一定的影响,主要体现在以下几个方面:
-
内存使用压力增大:
- 大量的对象实例占用堆内存空间,可能会导致堆内存使用率急剧上升。
- 如果不及时回收无用对象,会造成内存溢出(OutOfMemoryError)的风险。
-
垃圾回收压力增大:
- 大量短暂对象的频繁创建和销毁,会增加Java虚拟机的垃圾回收压力。
- 垃圾回收频率提高,可能会导致应用的性能下降。
-
线程间资源竞争:
- 多个线程同时在堆内存中分配和释放对象,可能会产生线程安全问题。
- 如果对象的创建和销毁不够规范,可能会导致内存泄漏等问题。
-
缓存命中率下降:
- 大量短暂对象的创建可能会污染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运行在不同的机器或者虚拟机上)。原因:
-
UUID生成算法: 在Java中,UUID通常使用RFC 4122中定义的算法生成。这个算法利用了时间戳、MAC地址、随机数等多种因素来生成唯一的UUID。
-
时间因素: 算法中使用了时间戳作为生成因素之一。不同的JVM,即使在同一时间生成,由于机器时钟的微小差异,也会导致生成的UUID不同。
-
空间因素: 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进来,这时就会有两个线程执行这个业务(发生了并发),不符合我们的要求。