java锁定商品_java都为我们提供了各种锁,为什么还需要分布式锁?

本文探讨了在分布式系统中为何需要使用分布式锁,通过一个秒杀商品的案例,展示了从无锁到使用Java的`synchronized`关键字加锁,再到引入Redis分布式锁的过程,分析了`synchronized`在多进程环境中的局限性,并提出分布式锁的优化方案,包括异常处理、锁超时和防止锁永久失效等问题。
摘要由CSDN通过智能技术生成

目前的项目单体结构的基本上已经没有了,大多是分布式集群或者是微服务这些。既然是多台服务器。就免不了资源的共享问题。既然是资源共享就免不了并发的问题。针对这些问题,redis也给出了一个很好的解决方案,那就是分布式锁。这篇文章主要是针对为什么需要使用分布式锁这个话题来展开讨论的。前一段时间在群里有个兄弟问,既然分布式锁能解决大部分生产问题,那么java为我们提供的那些锁有什么用呢?直接使用分布式锁不就结了嘛。针对这个问题我想了很多,一开始是在网上找找看看有没有类似的回答。后来想了想。想要解决这个问题,还需要从本质上来分析。

OK,开始上车出发。

一、前言

既然是分布式锁,这就说明服务器不是一台,可能是很多台。我们使用一个案例,来一步一步说明。假设某网站有一个秒杀商品,一看还有100件,于是陕西、江苏、西藏等地的人都看到了这个活动,于是开始进行疯狂秒杀。假设这个秒杀商品的数量值保存在一个redis数据库中。

e0835fabac70c0ba5dddf709c04eda21.png

但是不同地区的用户使用不同的服务器进行秒杀。这样就形成了一个集群访问的方式。

3100d12ed860b91ff585421373d37480.png

方式我们使用Springboot来整合redis。

二、项目搭建准备

(1)添加pom依赖1        

2            org.springframework.boot

3            spring-boot-starter-web

4        

5        

6            org.springframework.boot

7            spring-boot-starter-test

8            test

9        

10        

11            org.springframework.boot

12            spring-boot-starter-data-redis

13        

14        

15            org.apache.commons

16            commons-pool2

17        

(2)添加属性配置1# Redis数据库索引(默认为0)

2spring.redis.database=0

3# Redis服务器地址

4spring.redis.host=localhost

5# Redis服务器连接端口

6spring.redis.port=6379

7# Redis服务器连接密码(默认为空)

8spring.redis.password=

9# 连接池最大连接数(使用负值表示没有限制) 默认 8

10spring.redis.lettuce.pool.max-active=8

11# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1

12spring.redis.lettuce.pool.max-wait=-1

13# 连接池中的最大空闲连接 默认 8

14spring.redis.lettuce.pool.max-idle=8

15# 连接池中的最小空闲连接 默认 0

16spring.redis.lettuce.pool.min-idle=0

(3)新建config包,创建RedisConfig类1@Configuration

2public class RedisConfig{

3    @Bean

4    public RedisTemplate

5            redisTemplate(LettuceConnectionFactory connectionFactory){

6        RedisTemplate redisTemplate = new RedisTemplate<>();

7        redisTemplate.setKeySerializer(new StringRedisSerializer());

8        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

9        redisTemplate.setConnectionFactory(connectionFactory);

10        return redisTemplate;

11    }

12}

(4)新建controller,创建Mycontroller类1@RestController

2public class MyController{

3    @Autowired

4    private StringRedisTemplate stringRedisTemplate;

5    @GetMapping("/test")

6    public String deduceGoods(){

7        int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

8        int realGoods = goods-1;

9        if(goods>0){

10            stringRedisTemplate.opsForValue().set("goods",realGoods+"");

11            return "你已经成功秒杀商品,此时还剩余:" + realGoods + "件";

12        }else{

13            return "商品已经售罄,欢迎下次活动";

14        }

15    }

16}

很简单的一个整合教程。这个端口是8080,我们复制一份这个项目,把端口改成8090,并且以nginx作负载均衡搭建集群。现在环境我们已经整理好了。下面我们就开始进行分析。

三、为什么需要分布式锁

阶段一:采用原生方式

我们使用多个线程访问8080这个端口。因为没有加锁,此时肯定会出现并发问题。因此我们可能会想到,既然这个goods是一个共享资源,而且是多线程访问的,就立马能想到java中的各种锁了,最有名的就是synchronized。所以我们不如对上面的代码进行优化。

阶段二:使用synchronized加锁

此时我们对代码修改一下:1@RestController

2public class MyController{

3    @Autowired

4    private StringRedisTemplate stringRedisTemplate;

5    @GetMapping("/test")

6    public String deduceGoods(){

7        synchronized (this){

8            int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

9            int realGoods = goods-1;

10            if(goods>0){

11                stringRedisTemplate.opsForValue().set("goods",realGoods+"");

12                return "你已经成功秒杀商品,此时还剩余:" + realGoods + "件";

13            }else{

14                return "商品已经售罄,欢迎下次活动";

15            }

16        }

17

18    }

19}

看到没,现在我们使用synchronized关键字加上锁,这样多个线程并发访问的时候就不会出现数据不一致等各种问题了。这种方式在单体结构下的确有用。目前的项目单体结构的很少,一般都是集群方式的。此时的synchronized就不再起作用了。为什么synchronized不起作用了呢?

我们采用集群的方式去访问秒杀商品(nginx为我们做了负载均衡)。就会看到数据不一致的现象。也就是说synchronized关键字的作用域其实是一个进程,在这个进程下面的所有线程都能够进行加锁。但是多进程就不行了。对于秒杀商品来说,这个值是固定的。但是每个地区都可能有一台服务器。这样不同地区服务器不一样,地址不一样,进程也不一样。因此synchronized无法保证数据的一致性。

阶段三:分布式锁

上面synchronized关键字无法保证多进程的锁机制,为了解决这个问题,我们可以使用redis分布式锁。现在我们把代码再进行修改一下:1@RestController

2public class MyController{

3    @Autowired

4    private StringRedisTemplate stringRedisTemplate;

5    @GetMapping("/test")

6    public String deduceGoods(){

7      Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");

8      if(!result){

9           return "其他人正在秒杀,无法进入";

10      }

11      int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

12      int realGoods = goods-1;

13      if(goods>0){

14          stringRedisTemplate.opsForValue().set("goods",realGoods+"");

15          System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");

16      }else{

17          System.out.println("商品已经售罄,欢迎下次活动");

18      }

19      stringRedisTemplate.delete("lock");

20      return "success";

21    }

22}

就是这么简单,我们只是加了一句话,然后进行判断了一下。其实setIfAbsent方法的作用就是redis中的setnx。意思是如果当前key已经存在了,就不做任何操作了,返回false。如果当前key不存在,那我们就可以操作。最后别忘了释放这个key,这样别人就可以再进来实时秒杀操作。

当然这里只是给出一个最基本的案例,其实分布式锁实现起来步骤还是比较多的,而且里面很多坑也没有给出。我们随便解决几个:

阶段四:分布式锁优化

(1)第一个坑:秒杀商品出现异常,最终无法释放lock分布式锁1 public String deduceGoods() throws Exception{

2    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");

3    if(!result){

4       return "其他人正在秒杀,无法进入";

5    }

6    try {

7       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

8       int realGoods = goods-1;

9       if(goods>0){

10           stringRedisTemplate.opsForValue().set("goods",realGoods+"");

11           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");

12       }else{

13           System.out.println("商品已经售罄,欢迎下次活动");

14       }

15    }finally {

16       stringRedisTemplate.delete("lock");

17    }

18    return "success";

19}

此时我们加一个try和finally语句就可以了。最终一定要删除lock。

(2)第二个坑:秒杀商品时间太久,其他用户等不及1public String deduceGoods() throws Exception{

2    stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);

3    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");

4    if(!result){

5       return "其他人正在秒杀,无法进入";

6    }

7    try {

8       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

9       int realGoods = goods-1;

10       if(goods>0){

11           stringRedisTemplate.opsForValue().set("goods",realGoods+"");

12           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");

13       }else{

14           System.out.println("商品已经售罄,欢迎下次活动");

15       }

16    }finally {

17       stringRedisTemplate.delete("lock");

18    }

19    return "success";

20}

给其添加一个过期时间,也就是说如果10毫秒内没有秒杀成功,就表示秒杀失败,换下一个用户。

(3)第三个坑:高并发场景下,秒杀时间太久,锁永久失效问题

我们刚刚设置的锁过期时间是10毫秒,如果一个用户秒杀时间是15毫秒,这也就意味着他可能还没秒杀成功,就有其他用户进来了。当这种情况过多时,就可能有大量用户还没秒杀成功其他大量用户就进来了。有可能其他用户提前删除了lock,但是当前用户还没有秒杀成功。最终造成数据的不一致。看看如何解决:1public String deduceGoods() throws Exception{

2    String user = UUID.randomUUID().toString();

3    stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);

4    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock",user);

5    if(!result){

6       return "其他人正在秒杀,无法进入";

7    }

8    try {

9       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));

10       int realGoods = goods-1;

11       if(goods>0){

12           stringRedisTemplate.opsForValue().set("goods",realGoods+"");

13           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");

14       }else{

15           System.out.println("商品已经售罄,欢迎下次活动");

16       }

17    }finally {

18       if(user.equals(stringRedisTemplate.opsForValue().get("lock"))){

19                 stringRedisTemplate.delete("lock");

20       }

21    }

22    return "success";

23}

也就是说,我们在删除lock的时候判断是不是当前的线程,如果是那就删除,如果不是那就不删除,这样就算别的线程进来也不会乱删lock,造成混乱。

OK,到目前为止基本上把分布式锁的缘由介绍了一遍。对于分布式锁redisson完成的相当出色,下篇文章也将围着绕Redisson来介绍一下分布式如何实现,以及其中的原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值