基于Redis实现分布式锁

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 基于Redis实现分布式锁

正文


一、Redis分布式锁原理


分布式锁需要满足以下几点要求


在分布式系统环境下,一个方法在同一时间只能被一个机器的的一个线程执行 即独享(相互排斥);


高可用的获取锁与释放锁 ;


高性能的获取锁与释放锁;


具备可重入特性;


具备锁失效机制,防止死锁;


具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。


目前市面上普遍使用的分布式锁实现的方式主要有三种一种是基于数据库,一种是基于Zookeeper(点击链接可查看基于Zk实现的分布式锁),还有一种就是现在说的基于Redis实现的分布式锁。本文只说单机状态下Redis的分布式锁。如果在主从复制或者集群模式下(如果你能允许下面这些小概率的事件发生,同样也可以用在集群或者主从复制的情况下),如果主节点数据还没有同步到从节点的时候,突然宕机了,然后从节点变为主节点,获取到了锁,就违背了分布式锁的相互排斥原则。


那么Redis实现分布式锁的原理是什么?


Redis实现分布式锁基于SetNx命令,因为在Redis中key是保证是唯一的。所以当多个线程同时的创建setNx时,只要谁能够创建成功谁就能够获取到锁。SetNx命令 每次SetNx检查该 key是否已经存在,如果已经存在的话不会执行任何操作,返回为0。 如果不存在的话直接新增该key,新增成功返回1。


二、Redis实现分布式锁


       基于Jedis实现


工具类

package com.xiaojie.utils;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
 * @ClassRedisPoolUtil
 * @Description jedispool连接工具
 * @AuthorAdministrator
 * @Date {Date}{Time}
 * @Version 1.0
 **/
@Component
public class RedisPoolUtil {
    private static String IP = "192.168.139.154";
    //Redis的端口号
    private static int PORT = 6379;
    //可用连接实例的最大数目,默认值为8;
    //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
    private static int MAX_ACTIVE = 100;
    //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
    private static int MAX_IDLE = 20;
    //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
    private static int MAX_WAIT = 3000;
    private static int TIMEOUT = 3000;
    //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
    private static boolean TEST_ON_BORROW = true;
    //在return给pool时,是否提前进行validate操作;
    private static boolean TEST_ON_RETURN = true;
    private static JedisPool jedisPool = null;
    /**
     * redis过期时间,以秒为单位
     */
    public final static int EXRP_HOUR = 60 * 60; //一小时
    public final static int EXRP_DAY = 60 * 60 * 24; //一天
    public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月
    /**
     * 初始化Redis连接池
     */
    private static void initialPool() {
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(MAX_ACTIVE);
            config.setMaxIdle(MAX_IDLE);
            config.setMaxWaitMillis(MAX_WAIT);
            config.setTestOnBorrow(TEST_ON_BORROW);
            jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "xiaojie");
        } catch (Exception e) {
            //logger.error("First create JedisPool error : "+e);
            e.getMessage();
        }
    }
    /**
     * 在多线程环境同步初始化
     */
    private static synchronized void poolInit() {
        if (jedisPool == null) {
            initialPool();
        }
    }
    /**
     * 同步获取Jedis实例
     *
     * @return Jedis
     */
    public synchronized static Jedis getJedis() {
        if (jedisPool == null) {
            poolInit();
        }
        Jedis jedis = null;
        try {
            if (jedisPool != null) {
                jedis = jedisPool.getResource();
            }
        } catch (Exception e) {
            e.getMessage();
            // logger.error("Get jedis error : "+e);
        }
        return jedis;
    }
    /**
     * 释放jedis资源
     *
     * @param jedis
     */
    public static void returnResource(final Jedis jedis) {
        if (jedis != null && jedisPool != null) {
            jedisPool.returnResource(jedis);
        }
    }
    public static Long sadd(String key, String... members) {
        Jedis jedis = null;
        Long res = null;
        try {
            jedis = getJedis();
            res = jedis.sadd(key, members);
        } catch (Exception e) {
            //logger.error("sadd  error : "+e);
            e.getMessage();
        }
        return res;
    }
}


获取锁


package com.xiaojie.lock;
import com.xiaojie.utils.RedisPoolUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.util.UUID;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 基于redis实现分布式锁
 * @date 2021/9/19 21:17
 */
@Component
public class RedisDistributeLock {
    @Autowired
    private RedisPoolUtil redisPoolUtil;
    /**
     * @description:
     * @param: key 键值
     * @param: notLockTimeOut 为获取锁的等待时间
     * @param: timeOut 键值过期时间
     * @return: java.lang.String
     * @author xiaojie
     * @date: 2021/9/19 21:15
     */
    public String getLock(String key, Integer notLockTimeOut, Long timeOut) {
        Jedis jedis = redisPoolUtil.getJedis();
        //计算锁的超时时间
        Long endTime = System.currentTimeMillis() + notLockTimeOut;
        //当前时间比锁的超时时间小,证明获取锁时间没有超时,继续获取
        while (System.currentTimeMillis() < endTime) {
            String lockValue = UUID.randomUUID().toString();
            //如果设置成功返回1 获取到锁,如果返回0 获取锁失败,继续获取
            if (1 == jedis.setnx(key, lockValue)) {
                //设置键值过期时间,防止死锁
                jedis.expire(key, timeOut);
                return lockValue;
            }
        }
        //关闭连接
        try {
            if (jedis != null) {
                jedis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * @description: 释放锁
     * @param: key
     * @return: boolean
     * @author xiaojie
     * @date: 2021/9/19 21:25
     */
    public boolean unLock(String key, String lockValue) {
        //获取Redis连接
        Jedis jedis = redisPoolUtil.getJedis();
        try {
            // 判断获取锁的时候保证自己删除自己
            if (lockValue.equals(jedis.get(key))) {
                return jedis.del(key) > 0 ? true : false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }
}


基于RedisTemplate实现


package com.xiaojie.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 基于RedisTemplate实现分布式锁
 * @date 2021/9/19 21:57
 */
@Component
public class RedisTemplateDistributeLock {
    private static final Long SUCCESS = 1L;
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * @description: 获取锁
     * @param: lockKey 键值
     * @param: value 唯一的值 区别那个获取到的锁
     * @param: expireTime 过期时间
     * @return: boolean
     * @author xiaojie
     * @date: 2021/9/19 22:15
     */
    public boolean getLock(String lockKey, String value, Long expireTime) {
        try {
            //lua脚本
            String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end else return 0 end";
            Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), value, expireTime);
            return SUCCESS.equals(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * @description: 释放锁
     * @param:
     * @param: lockKey
     * @param: value
     * @return: boolean
     * @author xiaojie
     * @date: 2021/9/19 22:18
     */
    public boolean releaseLock(String lockKey, String value) {
        try {
            //lua脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value);
            return SUCCESS.equals(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}


三、测试


分布式锁在分布式定时任务的测试


package com.xiaojie.schedule;
import com.xiaojie.entity.User;
import com.xiaojie.lock.RedisDistributeLock;
import com.xiaojie.lock.RedisTemplateDistributeLock;
import com.xiaojie.mapper.UserMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.UUID;
/**
 * @ClassMyTask
 * @Description 模拟定时任务
 * @AuthorAdministrator
 * @Date {Date}{Time}
 * @Version 1.0
 **/
@Component
public class MyTask {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisDistributeLock  redisDistributeLock;
    private static final String REDISKEY = "xiaojie_redis_lock";
    private static final Integer NOLOCK_TIMEOUT=5;
    private static final Long TIME_OUT=5L;
    private static final String value= UUID.randomUUID().toString();
     int i;
    @Scheduled(cron = "0/5 * * * * ? ")
    public void scheduledTask(){
        String lockValue = redisDistributeLock.getLock(REDISKEY, NOLOCK_TIMEOUT, TIME_OUT);
        if(StringUtils.isBlank(lockValue)){
            System.out.println("获取锁失败");
            return;
        }
        List<User> users = userMapper.selectAll();
        for (User user:users){
            user.setNum(user.getNum()+1);
            int updateNum = userMapper.updateNum(user.getNum(),user.getId());
            System.out.println(updateNum);
        }
        i++;
        System.out.println("执行了"+i+"次");
        //释放锁
        redisDistributeLock.unLock(REDISKEY,lockValue);
    }
}


在不加锁的情况下,开启两台机器


222.png

111.png

444.png


可见一个机器上运行了5次一次运行7次理论上应该是7+5=12,但是数据库结果却是8。

加上锁之后


333.png

222.png

111.png


一个执行了7次一个执行了2次 7+2=9,正好符合我们测试需要的结果。


完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码


参考:springboot的RedisTemplate实现分布式锁_long2010110的专栏-CSDN博客_redistemplate实现分布式锁

相关实践学习
基于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 Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
8天前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
41 16
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
59 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
47 1
|
1月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
74 4
|
1月前
|
缓存 NoSQL Ubuntu
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
55 3
|
1月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
|
机器学习/深度学习 缓存 NoSQL
|
缓存 NoSQL Java
为什么分布式一定要有redis?
1、为什么使用redis 分析:博主觉得在项目中使用redis,主要是从两个角度去考虑:性能和并发。当然,redis还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis。
1364 0
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6