Redis 分布式锁(下)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 问题:你知道 redis 分布式锁吗?有哪些实现方案?你谈谈对 redis 分布式锁的理解, 删除key 的时候有什么问题?

思考7: 解锁过程的原子性/保证事务


问题:


finally 块的判断 + del 删除操作不是原子性的


// 解锁, 判断加锁与解锁不是同一个客户端
if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) {
    // 若在此时,这把锁不是这个客户端的,则会错误的解锁
    stringRedisTemplate.delete(REDIS_LOCK);
}


解决方案,不用 lua 脚本,其他的操作


  • redis 本身的事务


事务介绍


  • redis 的事务是通过 MULTI、EXEC、DISCARD 和 WATCH 这四个命令来完成的。


  • redis 的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。


  • redis 将命令集合序列化确保处于一事务的命令集合连续且不被打断的执行。


  • redis 不支持回滚的操作


相关命令


序号 命令及描述
1 DISCARD 取消事务,放弃执行事务块内的所有命令。
2 EXEC 执行所有事务块内的命令。
3 MULTI 标记一个事务块的开始。
4 UNWATCH 取消 WATCH 命令对所有 key 的监视。
5 WATCH key [key ...] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。


命令执行


image.png


redis 自身的事务优化


@GetMapping("/buyGoods")
public String buyGoods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        // 加锁
        //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//NX
        // 10s 过期
        // stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//NX
        if (Boolean.FALSE.equals(flag)) {
            logger.info("抢锁失败, serverPort:{}", serverPort);
            return "抢锁失败";
        }
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.valueOf(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
            logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort);
            return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort;
        }
        logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort);
        return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort;
    } finally {
        // redis 事务
        while (true) {
            stringRedisTemplate.watch(REDIS_LOCK);
            if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) {
                stringRedisTemplate.setEnableTransactionSupport(true);
                stringRedisTemplate.multi();
                stringRedisTemplate.delete(REDIS_LOCK);
                List<Object> list = stringRedisTemplate.exec();
                if (CollectionUtils.isEmpty(list)) {
                    continue;
                }
            }
            stringRedisTemplate.unwatch();
            break;
        }
    }
}


通过 lua 脚本的方式来删除


@GetMapping("/buyGoods")
    public String buyGoods() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            // 加锁
            //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//NX
            // 10s 过期
            // stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//NX
            if (Boolean.FALSE.equals(flag)) {
                logger.info("抢锁失败, serverPort:{}", serverPort);
                return "抢锁失败";
            }
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.valueOf(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort);
                return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort;
            }
            logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort);
            return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort;
        } finally {
            // 解锁
            /*
            if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) {
                stringRedisTemplate.delete(REDIS_LOCK);
            }
            */
            /*
            while (true) {
                stringRedisTemplate.watch(REDIS_LOCK);
                if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) {
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();
                    stringRedisTemplate.delete(REDIS_LOCK);
                    List<Object> list = stringRedisTemplate.exec();
                    if (CollectionUtils.isEmpty(list)) {
                        continue;
                    }
                }
                stringRedisTemplate.unwatch();
                break;
            }
            */
            String RELEASE_LOCK_LUA_SCRIPT = "if redis.call("get",KEYS[1]) == ARGV[1] then\n" +
                    "    return redis.call("del",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            DefaultRedisScript<Long> redisScript =
                    new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT, Long.class);
            // 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
            Long result = stringRedisTemplate.execute(redisScript,
                    Collections.singletonList(REDIS_LOCK), value);
            if (Objects.equals(result, 1)) {
                logger.info(" ========> del redis lock ok!");
            } else {
                logger.info(" ========> del redis lock err!");
            }
        }
    }


思考8: 终极解决方案还是得用 redisson


确保 redislock 过期时间大于业务执行时间的问题


老身长谈的问题:redis 分布式锁如何续期


集群 + CAP 对比 zookeeper


  • redis - ap ; redis 异步复制造成的锁丢失,比如:主节点还没来得及把刚刚 set 进来的这条数据给从节点,就挂了


  • zookeeper - cp


总结


redis 集群环境下,我们字节写的也不 ok, 直接使用 redlock 之 redisson 落地实现

上 redisson


  • 导入依赖


implementation 'org.redisson:redisson-spring-boot-starter:3.16.8'


  • 配置类 (java)


@Bean
public Redisson redisson() {
    Config config = new Config();
    config.useSingleServer().setAddress("http://127.0.0.1:6379").setDatabase(0);
    return (Redisson) Redisson.create(config);
}


  • 代码调整


@GetMapping("/buyGoods")
public String buyGoods() {
    RLock rLock = redisson.getLock(REDIS_LOCK);
    rLock.lock();
    try {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.valueOf(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
            logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort);
            return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort;
        }
        logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort);
        return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort;
    } finally {
        if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
            rLock.unlock();
        }
    }
}


Redis 分布式锁总结



1、synchronized 可以解决单机问题,但是对于我们生产分布式环境,需要通分布式锁来解决数据操作的原子性


2、通过 niginx 来建立分分布式微服务环境(验证单机锁的问题)


3、取消单机锁,上 redis 分布式锁 setnx


4、只是加锁,没有释放锁,对于出现异常的情况,可能无法释放锁,必须要在代码层面 finally 释放锁


5、宕机了,部署了微服务代码层面根本没有走 finally 中的代码语句,没有保证解锁,这个 key 没有被删除,所以我们需要有 lockKey 的过期时间设定


6、为 redis 的分布式锁 key , 增加过期时间,此外,还必须要 setnx + 过期时间必须同一行


7、必须规定只能自己删除自己的锁,你不能把别人的的锁删除了,防止张冠李戴

1 删 2 , 2 删 3.


8、redis 集群环境下,我们自己写的也不 ok 直接上 redlock 之 redisson 落地实现。


相关实践学习
基于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
相关文章
|
14天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
112 2
|
9天前
|
监控 NoSQL 算法
探秘Redis分布式锁:实战与注意事项
本文介绍了Redis分区容错中的分布式锁概念,包括利用Watch实现乐观锁和使用setnx防止库存超卖。乐观锁通过Watch命令监控键值变化,在事务中执行修改,若键值被改变则事务失败。Java代码示例展示了具体实现。setnx命令用于库存操作,确保无超卖,通过设置锁并检查库存来更新。文章还讨论了分布式锁存在的问题,如客户端阻塞、时钟漂移和单点故障,并提出了RedLock算法来提高可靠性。Redisson作为生产环境的分布式锁实现,提供了可重入锁、读写锁等高级功能。最后,文章对比了Redis、Zookeeper和etcd的分布式锁特性。
110 16
探秘Redis分布式锁:实战与注意事项
|
10天前
|
NoSQL Java 大数据
介绍redis分布式锁
分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。
|
11天前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
12天前
|
NoSQL Redis 微服务
分布式锁_redis实现
分布式锁_redis实现
|
16天前
|
NoSQL Java Redis
Redis入门到通关之分布式锁Rediision
Redis入门到通关之分布式锁Rediision
15 0
|
16天前
|
NoSQL 关系型数据库 MySQL
Redis入门到通关之Redis实现分布式锁
Redis入门到通关之Redis实现分布式锁
18 1
|
2月前
|
NoSQL Java Redis
如何通俗易懂的理解Redis分布式锁
在多线程并发的情况下,我们如何保证一个代码块在同一时间只能由一个线程访问呢?
40 2
|
1月前
|
NoSQL Java Redis
redis分布式锁
redis分布式锁
|
2月前
|
缓存 NoSQL Java
分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson(一)
分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson
60 0