java为我们已经提供了各种锁,为什么还需要分布式锁?

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 目前的项目单体结构的基本上已经没有了,大多是分布式集群或者是微服务这些。既然是多台服务器。就免不了资源的共享问题。既然是资源共享就免不了并发的问题。针对这些问题,redis也给出了一个很好的解决方案,那就是分布式锁。这篇文章主要是针对为什么需要使用分布式锁这个话题来展开讨论的。

一、前言


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

v2-10688d00fafad1f261cb603d0abd6c2f_1440w.jpg

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

v2-ccb8761d7e11012973c1825f3ac1cf61_1440w.jpg

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


二、项目搭建准备


(1)添加pom依赖


<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

(2)添加属性配置

# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0

(3)新建config包,创建RedisConfig类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Serializable> 
            redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

(4)新建controller,创建Mycontroller类

@RestController
public class MyController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/test")
    public String deduceGoods(){
        int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
        int realGoods = goods-1;
        if(goods>0){
            stringRedisTemplate.opsForValue().set("goods",realGoods+"");
            return "你已经成功秒杀商品,此时还剩余:" + realGoods + "件";
        }else{
            return "商品已经售罄,欢迎下次活动";
        }
    }
}

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


三、为什么需要分布式锁


阶段一:采用原生方式


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


阶段二:使用synchronized加锁


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

@RestController
public class MyController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/test")
    public String deduceGoods(){
        synchronized (this){
            int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
            int realGoods = goods-1;
            if(goods>0){
                stringRedisTemplate.opsForValue().set("goods",realGoods+"");
                return "你已经成功秒杀商品,此时还剩余:" + realGoods + "件";
            }else{
                return "商品已经售罄,欢迎下次活动";
            }
        }
    }
}

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


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


阶段三:分布式锁


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

@RestController
public class MyController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/test")
    public String deduceGoods(){
      Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");
      if(!result){
           return "其他人正在秒杀,无法进入";
      }
      int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
      int realGoods = goods-1;
      if(goods>0){
          stringRedisTemplate.opsForValue().set("goods",realGoods+"");
          System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");
      }else{
          System.out.println("商品已经售罄,欢迎下次活动");
      }
      stringRedisTemplate.delete("lock");
      return "success";
    }
}

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


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


阶段四:分布式锁优化


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


public String deduceGoods() throws Exception{
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");
    if(!result){
       return "其他人正在秒杀,无法进入";
    }
    try {
       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
       int realGoods = goods-1;
       if(goods>0){
           stringRedisTemplate.opsForValue().set("goods",realGoods+"");
           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");
       }else{
           System.out.println("商品已经售罄,欢迎下次活动");
       }
    }finally {
       stringRedisTemplate.delete("lock");
    }
    return "success";
}

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


(2)第二个坑:秒杀商品时间太久,其他用户等不及


public String deduceGoods() throws Exception{
    stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","冯冬冬");
    if(!result){
       return "其他人正在秒杀,无法进入";
    }
    try {
       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
       int realGoods = goods-1;
       if(goods>0){
           stringRedisTemplate.opsForValue().set("goods",realGoods+"");
           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");
       }else{
           System.out.println("商品已经售罄,欢迎下次活动");
       }
    }finally {
       stringRedisTemplate.delete("lock");
    }
    return "success";
}

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


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


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

public String deduceGoods() throws Exception{
    String user = UUID.randomUUID().toString();
    stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock",user);
    if(!result){
       return "其他人正在秒杀,无法进入";
    }
    try {
       int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
       int realGoods = goods-1;
       if(goods>0){
           stringRedisTemplate.opsForValue().set("goods",realGoods+"");
           System.out.println("你已经成功秒杀商品,此时还剩余:" + realGoods + "件");
       }else{
           System.out.println("商品已经售罄,欢迎下次活动");
       }
    }finally {
       if(user.equals(stringRedisTemplate.opsForValue().get("lock"))){
                 stringRedisTemplate.delete("lock");
       }
    }
    return "success";
}

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


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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
4月前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
5月前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
179 5
|
10月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
10月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
4月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
248 17
|
4月前
|
监控 NoSQL Java
分布式锁—2.Redisson的可重入锁
本文主要介绍了Redisson可重入锁RedissonLock概述、可重入锁源码之创建RedissonClient实例、可重入锁源码之lua脚本加锁逻辑、可重入锁源码之WatchDog维持加锁逻辑、可重入锁源码之可重入加锁逻辑、可重入锁源码之锁的互斥阻塞逻辑、可重入锁源码之释放锁逻辑、可重入锁源码之获取锁超时与锁超时自动释放逻辑、可重入锁源码总结。
|
6月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
336 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
6月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
231 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
6月前
|
存储 架构师 安全
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
锁状态bits1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode001偏向锁线程ID101轻量级锁指向栈中锁记录的指针000重量级锁指向互斥量的指针010尼恩提示,讲完 如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等优化手段 , 可以得到 120分了。如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等‌。JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。也是核心面试题。
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
|
5月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
162 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现

热门文章

最新文章