首先,我们得知道,锁可以分为三种
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
我们都知道,类似于京东,淘宝这样发平台都有抢购的活动,譬如说一双AJ,原价是2500,现在搞活动,前一千名只要999,那么这个时候就要涉及到分布式锁,如果我们不使用,那会有什么后果呢?
这里我们将使用jmeter工具来进行压力测试,我们现在Redis里存一个key为count,value为10000+的数据,若是这个还不会,请先学习Redis
@RequestMapping("/miaosha1")
public String Miaoha1(){
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
count=count-1;
System.out.println("当前库存::"+count);
srt.opsForValue().set("count", String.valueOf(num));
return "抢购成功";
}
return "抢购失败";
}
当并发量比较小的时候,譬如为10-30,这个时候好像没有什么问题,数据是正常减少的
那么,当并发量多起来了,结果会是什么样子的呢?
可以看到,当并发量稍微多了一点,数据都出现了很大的问题,很多时候读取的时候,库存都是没有减少的,这样就会导致本来只卖一千个的商品顿时卖出去了两千,甚至三千个,商家损失巨大,那么这个时候怎么办呢?当然是杀程序员祭天
说到锁,我们最先想到的自然就是synchronized,但是我们知道,synchronized只可以锁住同一个线程的,现在稍微大一点的应用都是微服务,分布式,如果是从不同的进程开始使用,那么会怎么办呢?
我们先看看单个下使用synchronized
@RequestMapping("/miaosha2")
public String Miaoha2(){
synchronized (this){
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
int num=count-1;
System.out.println("当前库存::"+num);
srt.opsForValue().set("count", String.valueOf(num));
return "抢购成功";
}
return "抢购失败";
}
}
我们可以看到,在单个线程下面,synchronized成功的锁住了,使数据保证了一致性,没有出现一个线程访问的数据和另一个线程访问时的数据一样的情况,但是,当我们分布式的调用,又会是什么情况呢?答案很明显,数据还是会错误,又要杀程序员祭天了。
我们已经知道,synchronized无法处理分布式环境下高并发的问题,那么这个时候,我们就可以使用Redis来进行一种”加锁“,来对抗分布上下,高并发环境带来的各种问题
我们来写一个简单的小demo,逻辑很简单,我们先设定一个key的前缀,比方说product,然后拼接上对应商品的id,形成一个key,value暂时可以随便设置一下,当第一个线程访问过来时,会生成一个缓存,如果这个时候有第二个线程过来,经过判断发现这里有缓存,则不会执行下面的语句,当第一个线程执行完成后,删除缓存,第三第四个线程继续执行,至于第二个线程,那不好意思,你运气不好,没抢到AJ
@RequestMapping("/miaosha3")
public String MiaoSha3(){
String key="product::001";
Boolean flag=srt.opsForValue().setIfAbsent(key,"icee");
if (flag){
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
int num=count-1;
System.out.println("当前库存::"+num);
srt.opsForValue().set("count", String.valueOf(num));
srt.delete(key);
return "抢购成功";
}else {
srt.delete(key);
return "存货不足";
}
}else {
return "网络错误";
}
}
这样看起来,似乎已经解决了,万事大吉,但是仔细一看,好像还有很多的问题,假如在删除缓存的前一个步骤,发生了某种错误,导致清除缓存的语句没有执行,那么会怎么办?
@RequestMapping("/miaosha4")
public String MiaoSha4(){
String key="product::001";
Boolean flag=srt.opsForValue().setIfAbsent(key,"icee");
if (flag){
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
int num=count-1;
System.out.println("当前库存::"+num);
srt.opsForValue().set("count", String.valueOf(num));
int g=10/0;
srt.delete(key);
return "抢购成功";
}else {
srt.delete(key);
return "存货不足";
}
}else {
return "网络错误";
}
}
我们可以看到,当发生这种情况的时候,接下来的线程都不会执行库存-1的语句了,那么商家就卖不出商品了,那还能怎么办?再杀一个程序员祭天。
出现这种情况,其实也很好解决,写一个try,finally就可以了,但是大家仔细想一想,如果这次不是出现异常,而是我们的系统他因为某种特殊原因,他挂了,而且这个时候好巧不巧的,刚好没执行删除缓存的语句,那么这个时候要怎么办?很简单,我们设置一个缓存的时长就可以了。
@RequestMapping("/miaosha5")
public String MiaoSha5(){
String key="product::001";
Boolean flag=srt.opsForValue().setIfAbsent(key,"icee",10,TimeUnit.SECONDS);
if (flag){
try {
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
int num=count-1;
System.out.println("当前库存::"+num);
srt.opsForValue().set("count", String.valueOf(num));
return "抢购成功";
}else {
return "存货不足";
}
}finally {
srt.delete(key);
}
}else {
return "网络错误";
}
}
这个时候代码看上去好像已经万无一失了,但是其实还是有问题,随着并发量增加,有时候一个线程的访问速度可能会变慢,假如,第一个线程再执行到清除缓存这一语句的时候花费了大量的时间,然后缓存过期了,这个时候第二个线程就可以继续执行了,然后第二个线程又创建了缓存,再第二个线程创建完缓存后,第一个线程立马执行了清除缓存的命令,这样子,第三,第四个线程也会持续下去,这样相当于没有加锁,那么,我们要怎么办呢?很简单,我们设置一个唯一的标识就可以了
@RequestMapping("/miaosha6")
public String MiaoSha6(){
String key="product::001";
String code= UUID.randomUUID().toString();
Boolean flag=srt.opsForValue().setIfAbsent(key,code,10,TimeUnit.SECONDS);
if (flag){
try {
int count= Integer.parseInt(srt.opsForValue().get("count"));
if (count>0){
int num=count-1;
System.out.println("当前库存::"+num);
srt.opsForValue().set("count", String.valueOf(num));
return "抢购成功";
}else {
return "存货不足";
}
}finally {
if (srt.opsForValue().get(key).equals(code)){
srt.delete(key);
}
}
}else {
return "网络错误";
}
}
这样子看起来,代码就万无一失了,但是,还是有小问题,再更高的并发情况下,假如当第一个线程判断唯一标识符是正确的,准备删除缓存的时候,刚好缓存也过期了,第二个线程也创建了缓存,那么第一个线程就会把第二个线程的锁删除了,这里就要涉及到锁续命和redisson了,在这里就不讲了,目前本文所讲的,已经足够应付中小企业的项目了。
先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦