微服务架构-Redis实战原理剖析-075:基于Redis实现分布式锁

1 Redis中为什么没有回滚的事务

课程内容:
1.分布式锁常见有哪些解决方案?
2.基于Redis实现分布式锁的思路
3.基于Redis实现分布式锁超时控制
4.对比与Zookeeper实现分布式锁的区别

为什么redis不支持回滚事务?
Mysql开启事务,对该行数据上行锁,commit数据提交,回滚对事务和行锁都会撤销;redis没有对数据上锁,没必要回滚事务,直接取消事务(不提交事务)即可。

2 什么是分布式锁?应用场景有哪些

什么是分布式锁? 举个场景
本地锁:在多个线程中,保证只有一个线程执行(线程安全的问题)
分布锁:在分布式中,保证只有一个jvm执行(多个jvm线程安全问题)
如果当前服务器是集群的时候,定时任务可能会重复执行
可以采用分布式锁解决,谁抢到锁谁执行任务。

3 实现分布式锁核心的思路

分布式锁实现方案:

  1. 基于数据库方式实现
  2. 基于Zk方式实现 采用临时节点+事件通知
  3. 基于Redis方式实现 setnx 命令方式

实现分布式锁核心思路:
1.获取锁
多个不同的jvm 同时创建一个相同的标记(全局唯一的),只要谁能够创建成功谁就能够获取锁;
2.释放锁
释放该全局唯一的标记,其他的jvm重新进入到获取锁资源;
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;

4 基于Redis实现分布式锁的思路

分析:基于Redis实现分布式锁思路
1.获取锁
多个不同的jvm 同时创建一个相同的标记,使用Setnx命令,因为Redis key必须保证是唯一的,只要谁能够创建成功谁就能够获取锁;
Set命令的时候:如果key不存在则创建,如果key已经存在则修改原值;
SetNx命令: 如果key不存在则创建返回1;如果已经存在则不执行任何操作返回0;
2.释放锁
对redis的key设置一个有效期(或者是主动删除该key)可以灵活的自动释放该全局唯一的标记,其他的jvm重新进入到获取锁资源;
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;

5 基于Redis实现获取锁代码实现

Maven依赖

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>
</dependencies>

RedisUtil

public class RedisUtil {
    //protected static Logger logger = Logger.getLogger(RedisUtil.class);
    
    private static String IP = "192.168.0.54";

    //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, "123456");
        } 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;
    }
}

6 基于Redis实现释放锁代码实现

public class MayiktRedisLock {

    private static int LockSuccess = 1;

    /**
     * @param lockKey     在Redis中创建的key
     * @param notLockTime 尝试获取锁的超时时间
     * @return 返回lock锁成功标记值
     */
    public String getLock(String lockKey, int notLockTime, int timeout) {

        // 获取redis连接
        Jedis jedis = RedisUtil.getJedis();
        // 计算尝试获取锁的超时时间
        Long endTime = System.currentTimeMillis() + notLockTime;
        // 当前系统时间小于endTime,说明获取锁没有超时,继续循环,否则退出循环
        while (System.currentTimeMillis() < endTime) {
            String lockValue = UUID.randomUUID().toString();
            // 多个不同的jvm同时创建一个相同的redis key,谁创建成功谁获取锁,返回值1
            if (jedis.setnx(lockKey, lockValue) == LockSuccess) {
                // 加上key有效期
                jedis.expire(lockKey, timeout / 1000);
                // 退出循环
                return lockValue;
            }
            // 否则情况下继续循环重试
            try {
                if (jedis != null) {
                    jedis.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }


    /**
     * 释放锁
     *
     * @param lockKey
     * @param lockValue
     * @return
     */
    public boolean unLock(String lockKey, String lockValue) {
        // 获取redis连接
        Jedis jedis = RedisUtil.getJedis();
        try {
            // 根据lockValue确定对应的JVM
            if (lockValue.equals(jedis.get(lockKey))) {
                return jedis.del(lockKey) > 0 ? true : false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }
}

7 测试基于Redis实现分布式锁代码

public class OrderService {

    private static final String LOCKKEY = "mayikt_lock";

    public static void service() {
        // 1.获取锁
        MayiktRedisLock mayiktRedisLock = new MayiktRedisLock();
        String lockValue = mayiktRedisLock.getLock(LOCKKEY, 5000, 5000);
        if (StringUtils.isEmpty(lockValue)) {
            System.out.println(Thread.currentThread().getName() + ",获取锁失败");
            return;
        }
        // 执行正常业务逻辑
        System.out.println(Thread.currentThread().getName() + ",获取锁成功,lockValue:" + lockValue);
        // 3.释放锁
        mayiktRedisLock.unLock(LOCKKEY, lockValue);
    }

    public static void main(String[] args) {
        service();
    }
}

运行结果:
在这里插入图片描述

8 在使用分布式锁如果超时了,如何处理

尝试获取锁为什么没有次数限制?
根据时间判断更合理,notLockTime,尝试获取锁的超时时间。

如果业务逻辑没有在5s内(设置的锁过期时间)执行完毕?
1.锁的超时时间根据业务场景来预估;
2.出现异常可以手动延长锁的超时时间(不建议);
3.在提交事务的时候检查锁是否已经超时,计算时间差判断是不是在锁的超时时间范围内,如果已经超时则手动回滚,否则提交。

9 zk与Redis实现分布式锁的优缺点

分析基于Zk实现分布式锁思路
1.获取锁
多个不同的jvm在zk集群上创建一个相同的全局唯一的临时路径,只要谁能够创建成功谁就能够获取到锁。
分析:临时节点相当于对节点设置有效期
2.释放锁(zk效率没有redis高)
人为主动删除该节点或者使用Session有效期
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;

Redis也可以实现类似于zk事件通知,key过期通知。
Redis官方提供一个redis解决分布锁框架redission。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值