使用Redis实现分布式锁

分布式锁

  在分布式的环境下,多个进程不再同一个系统中,控制多个进程对资源对访问

使用场景

  一台机器上多个不同线程抢占同一个资源,多次执行会有异常,我们称之为非线程安全, 可以通过synchronized或lock加锁解决,但如果是多台机器上多个不同线程抢占同一个资源就需要使用分布式锁。

实践

  1、安装Redis并启动

  2、创建Spring boot项目,引入Redis依赖

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.9.0</version>
</dependency>

  3、创建Redis连接池

  Redis 是单进程单线程的,它利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

  Redis 是基于内存的数据库,使用之前需要建立连接,建立断开连接需要消耗大量的时间。

  连接池则可以实现在客户端建立多个连接并且不释放,当需要使用连接的时候通过一定的算法获取已经建立的连接,使用完了以后则还给连接池,这就免去了数据库连接所占用的时间。

  Redis 在收到多个连接后,采用的是非阻塞 IO,基于epoll的多路IO复用,然后采用队列模式将并发访问变为串行访问。

@Configuration
public class RedisPool {
    private JedisPool jedisPool;

    @Value("${redis.pool.maxTotal}")
    private int maxTotal;//最大连接数

    @Value("${redis.pool.maxIdle}")
    private int maxIdle;//最大空闲连接数

    @Value("${redis.pool.minIdle}")
    private int minIdle;//最小空闲连接数

    @Value("${redis.pool.testOnBorrow}")
    private boolean testOnBorrow;//在取连接时测试连接的可用性

    @Value("${redis.pool.testOnReturn}")
    private boolean testOnReturn;//再还连接时不测试连接的可用性

    @Value("${redis.pool.host}")
    private String host;//redis地址

    @Value("${redis.pool.port}")
    private int port;//端口

    @Value("${redis.pool.timeout}")
    private int timeout;

    @Value("${redis.pool.password}")
    private String password;

    public Jedis getJedis() {
        return jedisPool.getResource();
    }

    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);
        config.setBlockWhenExhausted(true);
        jedisPool = new JedisPool(config, host, port, timeout, null);
        return jedisPool;
    }
}

  4、创建加锁、解锁方法

  加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)

  NX参数可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。
  对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁,不会发生死锁。
  将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端

  

  解锁需要两行代码

  第一行代码,写了一个简单的Lua脚本
  第二行代码,将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行,eval()方法可以确保原子性

@Service
public class RedisPoolUtil {
    private static final String DEL_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    /**
     * NX:key不存在时才把key、value添加到redis
     * XX:key存在时才把key、value添加到redis
     */
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * EX:单位秒
     * PX:单位毫秒
     */
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    private static final Long ONE = 1L;
    private static final String OK = "OK";

    @Autowired
    private RedisPool redisPool;

    public boolean lock(String key, String requestId, int expireTime) {
        Object result = null;
        Jedis jedis = null;
        try {
            jedis = redisPool.getJedis();
            result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return OK.equals(result);
    }

    public boolean unLock(String lockKey, String requestId) {
        Jedis jedis = null;
        Object result = null;
        try {
            jedis = redisPool.getJedis();
            result = jedis.eval(DEL_LUA, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return ONE.equals(result);
    }
}

 

posted @ 2020-07-31 14:47  你好。世界!  阅读(229)  评论(0)    收藏  举报