使用Redis实现分布式锁与缓存策略方式

 更新时间:2025年11月25日 09:39:40   作者:百思可瑞教育  
文章介绍了Redis在分布式系统中实现分布式锁和缓存策略的优势,并详细阐述了SETNX+EXPIRE、使用Lua脚本、SETEXPXNX命令和Redisson框架等几种常见的分布式锁实现方案,同时,文章还探讨了旁路缓存、缓存穿透、缓存雪崩和缓存击穿等几种缓存策略

一、引言

在分布式系统中,多个进程或服务常常需要访问共享资源,为了保证数据的一致性和系统的稳定性,需要引入分布式锁机制。同时,为了提高系统的性能和响应速度,缓存策略也是必不可少的。

Redis凭借其原子性操作、内存存储、过期机制和分布式特性,成为实现分布式锁和缓存策略的理想选择。

二、Redis实现分布式锁

(一)分布式锁的意义

在单服务环境下,使用synchronized关键字可以保证线程安全,但在分布式系统中,多个节点访问同一个公共资源时,synchronized就无法发挥作用。

分布式锁能够确保在任意时刻,只有一个客户端能持有锁,防止多个客户端同时对共享资源进行操作,从而保证数据的一致性。

(二)Redis实现分布式锁的常见方案

SETNX + EXPIRE方案

原理

  • SETNXSET IF NOT EXISTS的简写,即当指定的键不存在时,为其设置值。
  • 先使用SETNX命令尝试获取锁,如果返回1,表示获取成功,再使用EXPIRE命令为锁设置一个过期时间,防止锁忘记释放导致死锁。

代码示例(Java)

import redis.clients.jedis.Jedis;

public class RedisLockExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        String key = "resource_lock";
        String value = "lock_value";
        int expireTime = 100; // 过期时间,单位秒

        if (jedis.setnx(key, value) == 1) {
            jedis.expire(key, expireTime);
            try {
                // 业务代码
                System.out.println("获取锁成功,执行业务逻辑");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.del(key);
                System.out.println("释放锁");
            }
        } else {
            System.out.println("获取锁失败");
        }
        jedis.close();
    }
}
- **缺点**:`SETNX`和`EXPIRE`两个命令不是原子操作,如果在执行完`SETNX`后,进程崩溃或重启,`EXPIRE`命令未执行,锁将无法释放,导致其他客户端永远无法获取锁。

SETNX + value(系统时间 + 过期时间)方案

原理

  • 把过期时间放在SETNXvalue值里。如果加锁失败,拿出value值校验是否过期。
  • 加锁成功时,将系统时间加上设置的过期时间作为value存入Redis。
  • 如果锁已存在,获取锁的过期时间,若过期时间小于系统当前时间,表示锁已过期,通过getSet命令尝试获取锁。

代码示例(Java)

import redis.clients.jedis.Jedis;

public class RedisLockWithTimeExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        String key = "resource_lock";
        long expireTime = 10000; // 过期时间,单位毫秒

        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);

        if (jedis.setnx(key, expiresStr) == 1) {
            System.out.println("获取锁成功");
        } else {
            String currentValueStr = jedis.get(key);
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                String oldValueStr = jedis.getSet(key, expiresStr);
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    System.out.println("获取锁成功");
                } else {
                    System.out.println("获取锁失败,其他线程已更新锁");
                }
            } else {
                System.out.println("获取锁失败,锁未过期");
            }
        }
        jedis.close();
    }
}
- **缺点**:过期时间是客户端自己生成的,依赖系统时间,要求分布式环境下每个客户端的时间必须同步。锁过期时,并发多个客户端同时请求,都执行`getSet`,最终只有一个客户端加锁成功,但该客户端锁的过期时间可能被别的客户端覆盖。且该锁没有保存持有者的唯一标识,可能被别的客户端释放。

使用Lua脚本方案

原理

  • Lua脚本可以将一组Redis命令放在一次请求里完成,Redis会将脚本作为一个整体执行,保证了原子性。
  • 通过Lua脚本实现SETNXEXPIRE两条指令的原子操作。

代码示例(Java)

import redis.clients.jedis.Jedis;
import java.util.Collections;

public class RedisLockWithLuaExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        String key = "resource_lock";
        String value = "lock_value";
        int expireTime = 100; // 过期时间,单位秒

        String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
                "redis.call('expire', KEYS[1], ARGV[2]) " +
                "else " +
                "return 0 " +
                "end";

        Object result = jedis.eval(luaScript, Collections.singletonList(key),
                Collections.singletonList(value + "," + expireTime));

        if (result.equals(1L)) {
            System.out.println("获取锁成功");
            try {
                // 业务代码
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.del(key);
                System.out.println("释放锁");
            }
        } else {
            System.out.println("获取锁失败");
        }
        jedis.close();
    }
}

SET的扩展命令(SET EX PX NX)方案

原理

  • Redis的SET指令有扩展参数[EX seconds][PX milliseconds][NX|XX],其中EX表示设置键的过期时间,单位为秒;PX表示设置键的过期时间,单位为毫秒;NX表示只有键不存在时才能设置成功。
  • 使用该命令可以原子性地完成设置键值和过期时间的操作。

代码示例(Java)

import redis.clients.jedis.Jedis;

public class RedisSetLockExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        String key = "resource_lock";
        String value = "lock_value";
        int expireTime = 100; // 过期时间,单位秒

        String result = jedis.set(key, value, "NX", "EX", expireTime);

        if ("OK".equals(result)) {
            System.out.println("获取锁成功");
            try {
                // 业务代码
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.del(key);
                System.out.println("释放锁");
            }
        } else {
            System.out.println("获取锁失败");
        }
        jedis.close();
    }
}

开源框架Redisson方案

原理

  • Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In - Memory Data Grid)框架,它提供了多种分布式锁的实现。
  • 当一个线程获得锁后,会开启一个定时守护线程,每隔一段时间检查锁是否还存在,若存在则延长锁的过期时间,防止锁过期提前释放。

代码示例(Java)

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonLockExample {
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        RedissonClient redissonClient = Redisson.create(config);

        RLock lock = redissonClient.getLock("resource_lock");

        try {
            boolean isLocked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
            if (isLocked) {
                System.out.println("获取锁成功");
                // 业务代码
            } else {
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            redissonClient.shutdown();
        }
    }
}

三、Redis缓存策略

(一)旁路缓存(Cache - Aside)策略

工作原理

  • 由应用层负责缓存和数据库的交互逻辑。
  • 读取数据时,先查询缓存,命中则直接返回;未命中则查询数据库,将结果写入缓存并返回。
  • 更新数据时,先更新数据库,再删除缓存(或更新缓存)。

代码示例(Java)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserServiceCacheAside {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private UserRepository userRepository;
    private static final String CACHE_KEY_PREFIX = "user:";
    private static final long CACHE_EXPIRATION = 30; // 缓存过期时间(分钟)

    public User getUserById(Long userId) {
        String cacheKey = CACHE_KEY_PREFIX + userId;
        // 1. 查询缓存
        User user = (User) redisTemplate.opsForValue().get(cacheKey);
        // 2. 缓存命中,直接返回
        if (user != null) {
            return user;
        }
        // 3. 缓存未命中,查询数据库
        user = userRepository.findById(userId).orElse(null);
        // 4. 将数据库结果写入缓存(设置过期时间)
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, user, CACHE_EXPIRATION, java.util.concurrent.TimeUnit.MINUTES);
        }
        return user;
    }

    public void updateUser(User user) {
        // 1. 先更新数据库
        userRepository.save(user);
        // 2. 再删除缓存
        String cacheKey = CACHE_KEY_PREFIX + user.getId();
        redisTemplate.delete(cacheKey);
    }
}

优缺点分析

  • 优点:实现简单,控制灵活;适合读多写少的业务场景;只缓存必要的数据,节省内存空间。
  • 缺点:首次访问会有一定延迟(缓存未命中);存在并发问题,如果先删除缓存后更新数据库,可能导致数据不一致;需要应用代码维护缓存一致性,增加了开发复杂度。

适用场景

  • 读多写少的业务场景;对数据一致性要求不是特别高的应用;分布式系统中需要灵活控制缓存策略的场景。

(二)缓存穿透解决方案

  1. 缓存空值:当查询的数据在数据库中不存在时,也在Redis中存入一个空值,并设置一个较短的过期时间。这样下次再查询该数据时,直接从缓存中返回空值,避免访问数据库。
  2. 布隆过滤器:布隆过滤器是一种概率型数据结构,用于快速判断一个元素是否存在于集合中。在访问缓存前,先通过布隆过滤器判断数据是否存在,若不存在则直接返回,避免访问缓存和数据库。

(三)缓存雪崩解决方案

  1. 设置不同的过期时间:为不同的缓存数据设置不同的过期时间,或者在过期时间上加上一个随机数,避免大量缓存数据同时过期。
  2. 部署高可用的Redis集群:通过主从节点的方式构建Redis高可靠集群,如果主节点故障,从节点可以切换为主节点继续提供服务,同时完善监控报警体系。

(四)缓存击穿解决方案

  1. 互斥锁:当缓存失效时,通过互斥锁保证同一时间只有一个请求去构建缓存,其他请求等待锁释放后再读取缓存。
  2. 逻辑过期:将缓存数据的过期时间存储在缓存中,当缓存过期时,不立即删除缓存,而是启动一个后台线程异步更新缓存。在读取缓存时,判断缓存是否过期,若过期则返回旧数据,并异步更新缓存。

四、总结

Redis在实现分布式锁和缓存策略方面具有显著的优势。通过多种分布式锁实现方案,可以满足不同场景下对锁的要求,保证共享资源的原子性访问。同时,合理的缓存策略能够提高系统的性能和响应速度,解决缓存穿透、雪崩和击穿等问题。在实际应用中,应根据具体的业务需求和系统特点,选择合适的分布式锁实现方案和缓存策略,以构建高效、稳定的分布式系统。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Redis总结笔记(二):C#连接Redis简单例子

    Redis总结笔记(二):C#连接Redis简单例子

    这篇文章主要介绍了Redis总结笔记(二):C#连接Redis简单例子,需要的朋友可以参考下
    2015-01-01
  • Redis定时监控与数据处理的实践指南

    Redis定时监控与数据处理的实践指南

    在现代分布式系统中,Redis作为高性能的内存数据库,常用于缓存、消息队列和实时数据处理,合理使用Redis数据结构,可以极大提升系统性能,本文将通过一个实际案例,介绍如何将Redis存储结构从 Set 迁移到Hash,并实现定时任务监控数据变化,需要的朋友可以参考下
    2025-06-06
  • 基于redis实现的点赞功能设计思路详解

    基于redis实现的点赞功能设计思路详解

    点赞是我们现在经常见到的一个效果,如朋友圈、微博都有点赞的效果,下面这篇文章主要跟大家分享了基于redis实现的点赞功能设计思路的相关资料,文中介绍的非常详细,对大家实现点赞功能具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-05-05
  • Spring Boot 整合Redis 实现优惠卷秒杀 一人一单功能

    Spring Boot 整合Redis 实现优惠卷秒杀 一人一单功能

    这篇文章主要介绍了Spring Boot 整合Redis 实现优惠卷秒杀 一人一单,在分布式系统下,高并发的场景下,会出现此类库存超卖问题,本篇文章介绍了采用乐观锁来解决,需要的朋友可以参考下
    2022-09-09
  • 远程连接阿里云服务器上的redis报错的问题解决

    远程连接阿里云服务器上的redis报错的问题解决

    本文主要介绍了远程连接阿里云服务器上的redis报错的问题,出现 Redis Client On Error: Error: connect ECONNREFUSED 47.100.XXX.XX:6379 错误,下面就来介绍一下解决方法,感兴趣的可以了解一下
    2025-04-04
  • 使用redis获取自增序列号实现方式

    使用redis获取自增序列号实现方式

    这篇文章主要介绍了使用redis获取自增序列号实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Redis数据迁移的多种方法详解

    Redis数据迁移的多种方法详解

    在现代的分布式系统中,Redis作为一种高性能的键值存储数据库,被广泛应用于缓存、消息队列、会话存储等场景,随着业务的发展,Redis实例的数据迁移需求也变得越来越常见,本文将详细介绍Redis数据迁移的多种方法,并通过命令行工具帮助你轻松完成迁移任务
    2025-01-01
  • 详解Redis如何处理Hash冲突

    详解Redis如何处理Hash冲突

    在 Redis 中,哈希表是一种常见的数据结构,通常用于存储对象的属性,对于哈希表,最常遇到的是哈希冲突,那么,当 Redis遇到Hash冲突会如何处理?本文我们将详细介绍Redis如何处理哈希冲突,需要的朋友可以参考下
    2024-09-09
  • Redis中的分布式锁之SETNX底层实现方式

    Redis中的分布式锁之SETNX底层实现方式

    这篇文章主要介绍了Redis中的分布式锁之SETNX底层实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-06-06
  • Redis全文搜索教程之创建索引并关联源数据的教程

    Redis全文搜索教程之创建索引并关联源数据的教程

    RediSearch提供了一种简单快速的方法对 hash 或者 json 类型数据的任何字段建立二级索引,然后就可以对被索引的 hash 或者 json 类型数据字段进行搜索和聚合操作,这篇文章主要介绍了Redis全文搜索教程之创建索引并关联源数据,需要的朋友可以参考下
    2023-12-12

最新评论