分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson(一)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson

title: 分布式项目中锁的应用(本地锁->redis【setnx】->redisson->springcache)
date: 2022-10-13 22:37:52.762
updated: 2022-10-13 22:42:50.637
url: /archives/fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson
categories:
tags: Redisson

[toc]

概述

1.适合放入缓存的数据

1.即时性、数据一致性要求不高的
2.访问量大且更新频率不高的数据(读多,写少)
举例:
  1.电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定)
  2.后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的
  3.物流信息

2.读模式缓存使用流程

3.本地缓存与局限性

1.集群情况下,每个节点的本地缓存可能会不一致(数据一致性)

4.分布式缓存

使用缓存中间件:
  redis(集群、分片)

整合redis

把redis看做Map

1.使用springboot整合redis

1.在需要使用redis的模块导入依赖,启动器
        <!--redis启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
2.RedisAutoConfiguration查看自动配置
在.yml增加以下配置
spring:
  redis:
    host: 192.168.56.10
    port: 6379
3.使用SpringBoot自动配置好的RedisTemplate或者StringRedisTemplate即可操作redis
【一般使用StringRedisTemplate】

2.测试用例

@Autowired
    StringRedisTemplate stringRedisTemplate;
    /**
     * 测试redis
     */
    @Test
    void testRedis() {
        // 获取操作对象
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        // 存储
        ops.set("hello", "world" + UUID.randomUUID());
        // 获取
        System.out.println(ops.get("hello"));
    }

3.lettuce堆外内存溢出(springboot2.3.2已解决)

3.1.lettuce、jedis、redistemplate

三者分别是什么?
  lettuce:redis的客户端,对redis操作进行封装,内部使用netty进行网络通信,性能很强
  jedis:redis的客户端,对redis操作进行封装,停止更新了
  redistemplate:是springboot对redis客户端的再封装

3.2.原因

异常描述:
  当进行压力测试时后期出现堆外内存溢出OutOfDirectMemoryError(压力测试指查询缓存数据)
  
原因:
  1)springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信,使用netty创建连接时未及时释放连接
  2)如果没有为netty指定对外内存,默认使用Xms的值(使用-Dio.netty.maxDirectMemory设置值)
解决:(只是调大堆外内存治标不治本)
  方法1:升级lettuce客户端(2.3.2已解决)
  方法2:切换使用jedis

3.3.解决方法:切换jedis

步骤:
排除lettuce依赖,导入jedis
<!--redis启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <!--排除springboot默认的redis客户端lettus-->
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--jedis,操作redis的客户端-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

4.缓存失效问题

读模式,会存在缓存失效问题:
  缓存穿透、雪崩、击穿

4.1.缓存穿透(不存在的数据)

缓存穿透:
  查询一个一定不存在的数据,导致一定会查询缓存+查询DB,缓存失去意义(大并发过来时任然会查询db)
风险:
  利用不存在的数据进行攻击,数据库顺时压力增大,最终导致崩溃
解决:
  方法1:将null结果缓存,并加入短暂过期时间
  弊端:查询条件使用UUID生成,仍然出现缓存穿透问题,并且redis存满了null
  
  方法2:布隆过滤器,不放行不存在的查询
    在redis维护id的hash表过滤掉id不存在的查询(不到达DB层查询)

4.2.缓存雪崩(大面积失效)

缓存雪崩:
  高并发状态下,大面积redis数据失效,导致所有查询到达DB,DB瞬时压力过重雪崩
  
解决:
  方法1:规避雪崩,设置随机的有效时间(实际上无需设置随机时间,因为每个缓存放入库中的时间本身就不固定)
    让每一个缓存过期时间重复率降低,
  
  方法2:永不失效
  方法3:
    事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
    事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
    事后:利用 redis 持久化机制保存的数据尽快恢复缓存 
问题:如果已经出现了缓存雪崩,如何解决?
  方法1:熔断、降级

4.3.缓存击穿(一条失效)

缓存击穿:
  高并发状态下,一条数据过期,所有请求到达DB
解决:
  方法1:加分布式锁
  例原子操作(Redis的SETNX或者Memcache的ADD)
  流程:查询cache失败,竞争锁,竞争成功查询cache,查询成功返回释放锁
    查询失败则查询DB,并set缓存,并释放锁
  方法2:永不失效

4.4.锁时效问题

结果放入缓存的操作,应该放在同步代码块内,否则会造成重复查询DB的情况

4.5.模拟分布式本地锁失效

1.启动多份配置
2.修改压测配置
  gulimall.com  80
    /index/catalog.json
3.开始压测
  100个线程  循环5次
4.本地锁失效,多次查询数据库

5.分布式锁

文档1:http://redisdoc.com/string/set.html
文档2:http://www.redis.cn/commands/set.html

5.1.演示分布式锁SETNX

1.打开多个sh框
2.打开xshell撰写栏(查看->撰写->撰写栏)
3.编辑命令,发送给多个窗口,同时连接redis客户端
docker exec -it redis redis-cli
4.编辑命令,发送给多个窗口,同时占锁
set key value NX
返回OK表示占锁成功,返回nill占锁失败
5.设置锁过期时间
set key value EX 300 NX
6.查看锁过期时间
ttl lock

发送命令至全部会话:

锁值:

5.2.问题合集

问题1:(删除锁)
  未执行删除锁逻辑,会导致其他线程无法获得锁,出现死锁
问题2:(设置过期时间)
    锁释放操作可能失败(服务宕机),所以需要设置过期时间
问题3:(设置过期时间的原子性)
    设置过期时间的代码必须在setnx抢占锁的同时设置,保证原子性
问题4:(仅可以删除当前线程占用的锁)
    删除锁时,可能锁已过期删除了其他线程的锁,占锁时设置值为uuid,删除时判断当前uuid是否相等
    并且需要使用lua脚本执行原子删除操作

5.3.redis分布式锁版本

/**
 * 查询三级分类(原生版redis分布式锁版本)
 */
public Map<String, List<Catalog2VO>> getCatalogJsonFromDBWithRedisLock() {
    // 1.抢占分布式锁,同时设置过期时间
    String uuid = UUID.randomUUID().toString();
    // 使用setnx占锁(setIfAbsent)
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(CategoryConstant.LOCK_KEY_CATALOG_JSON, uuid, 300, TimeUnit.SECONDS);
    if (isLock) {
        // 2.抢占成功
        Map<String, List<Catalog2VO>> result = null;
        try {
            // 查询DB
            return getCatalogJsonFromDB();
        } finally {
            // 3.查询UUID是否是自己,是自己的lock就删除
            // 封装lua脚本(原子操作解锁)
            // 查询+删除(当前值与目标值是否相等,相等执行删除,不等返回0)
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call('del',KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            // 删除锁
            redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList(CategoryConstant.LOCK_KEY_CATALOG_JSON), uuid);
        }
    } else {
        // 4.加锁失败,自旋重试
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogJsonFromDBWithRedisLock();
    }
}

Redisson

文档:
https://github.com/redisson/redisson/wiki/Table-of-Content

1.概述

1.不推荐直接使用SETNX实现分布式锁,应该使用Redisson
因为根据锁的实现会分为
  读写锁、可重入锁、闭锁、信号量、
2.封装了分布式Map、List等类型
3.Redisson与lettuce、jedis一样都是redis的客户端,代替了redisTemplate

2.使用原生redisson(看门狗)

步骤:
1.引入依赖
<!--redisson,redis客户端,封装了分布式锁实现,也可以使用springboot的方式,不需要自己配置-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.3</version>
</dependency>
2.配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class MyRedissonConfig {
    /**
     * 注入客户端实例对象
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson(@Value("${spring.redis.host}") String host, @Value("${spring.redis.port}")String port) throws IOException {
        // 1.创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);// 单节点模式
//        config.useSingleServer().setAddress("rediss://" + host + ":" + port);// 使用安全连接
//        config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");// 集群模式
        // 2.创建redisson客户端实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

2.1.可重入锁

redisson实现了JUC包下的可重入锁
RLock lock = redissonClient.getLock("redisson_lock");

2.2.过期时间、自动续期、手动释放(lua原子操作)

原理:
  // 1)默认过期时间30S
    // 2)锁自动续期+30S,业务超长情况下(看门狗)
    // 3)如果线程宕机,看门狗不会自动续期,锁会自动过期
    // 4)unlock使用lua脚本释放锁,不会出现误删锁
代码案例:
/**
 * 测试redisson实现分布式锁
 */
@ResponseBody
@GetMapping("/testRedisson")
public String test() {
    // 1.获取锁
    RLock lock = redissonClient.getLock("redisson_lock");
    // 2.加锁
    // 1)锁自动续期+30S,业务超长情况下(看门狗)
    // 2)如果线程宕机,看门狗不会自动续期,锁会自动过期
    // 3)unlock使用lua脚本释放锁,不会出现误删锁
    lock.lock();
    try {
        // 加锁成功,执行业务
        System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (Exception e) {
    } finally {
        // 3.解锁
        System.out.println("解锁..." + Thread.currentThread().getId());
        lock.unlock();
    }
    return "testRedisson";
}

2.3.指定超时不自动续期

1.查看源码
  1)当不指定超时时间时,默认30S过期,且启动一个定时任务【自动续期任务】
    续期时间点=默认过期时间/3,没隔10S执行一次续期
  2)当指定超时时间时,不会自动续期
2.推荐设置过期时间
  1)可以省略自动续期操作
  2)若真的超时未完成,则很有可能是数据库宕机,即使续期也无法完成,不应该无限续期下去
/**
 * 测试redisson实现分布式锁
 */
@ResponseBody
@GetMapping("/testRedisson")
public String test() {
    // 1.获取锁
    RLock lock = redissonClient.getLock("redisson_lock");
    // 2.加锁
    // 1)锁自动续期+30S,业务超长情况下(看门狗)
    // 2)如果线程宕机,看门狗不会自动续期,锁会自动过期
    // 3)unlock使用lua脚本释放锁,不会出现误删锁
    lock.lock();
    try {
        // 加锁成功,执行业务
        System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (Exception e) {
    } finally {
        // 3.解锁
        System.out.println("解锁..." + Thread.currentThread().getId());
        lock.unlock();
    }
    return "testRedisson";
}

2.4.tryLock

// 尝试加锁,最多等待100秒
// 超时时间30秒
lock.tryLock(100, 30, TimeUnit.SECONDS);

2.5.公平锁

// 有顺序进行加锁操作,按照请求的顺序
RLock lock = redisson.getFairLock("fair-lock");

2.6.读写锁

写+读:读阻塞
写+写:阻塞
读+写:写阻塞
RReadWriteLock rwlock = redisson.getReadWriteLock("lock");
// 读锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 写锁
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

写锁:

读锁:

读锁同时存入多个:

2.7.信号量Semphore

先设置一个值
  "park" 3
acquire:获取一个信号量,为0阻塞
release:释放一个信号量,+1
tryacquire:尝试获取一个信号量,不阻塞
作用:【限流】
  所有服务上来了去获取一个信号量,一个一个放行(最多只能n个线程同时执行)

2.8.闭锁CountDownLatch

// 等待一组操作执行完毕,统一执行

2.9.锁的粒度

锁的粒度一定要小,例如不应该锁整个商品操作,应该带上商品ID

2.10.redisson分布式锁版本

/**
 * 查询三级分类(redisson分布式锁版本)
 */
public Map<String, List<Catalog2VO>> getCatalogJsonFromDBWithRedissonLock() {
    // 1.抢占分布式锁,同时设置过期时间
    RLock lock = redisson.getLock(CategoryConstant.LOCK_KEY_CATALOG_JSON);
    lock.lock(30, TimeUnit.SECONDS);
    try {
        // 2.查询DB
        Map<String, List<Catalog2VO>> result = getCatalogJsonFromDB();
        return result;
    } finally {
        // 3.释放锁
        lock.unlock();
    }
}


分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson(二)https://developer.aliyun.com/article/1469579

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
NoSQL 安全 测试技术
Redis游戏积分排行榜项目中通义灵码的应用实战
Redis游戏积分排行榜项目中通义灵码的应用实战
61 4
|
2月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
21天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
106 26
|
17天前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
47 10
|
1月前
|
存储 运维 NoSQL
分布式读写锁的奥义:上古世代 ZooKeeper 的进击
本文作者将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。
|
27天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
85 5
|
1月前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
65 8
|
1月前
|
监控 NoSQL 网络协议
【Azure Redis】部署在AKS中的应用,连接Redis高频率出现timeout问题
查看Redis状态,没有任何异常,服务没有更新,Service Load, CPU, Memory, Connect等指标均正常。在排除Redis端问题后,转向了AKS中。 开始调查AKS的网络状态。最终发现每次Redis客户端出现超时问题时,几乎都对应了AKS NAT Gateway的更新事件,而Redis服务端没有任何异常。因此,超时问题很可能是由于NAT Gateway更新事件导致TCP连接被重置。
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
60 16
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
44 5