Redis實現分佈式鎖

分佈式鎖

 

  • 當在分佈式模型下,數據只有一份(或有限制),此時需要利用鎖的技術控制某一時刻修改數據的進程數。
  • 與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網絡問題。(我覺得分佈式情況下之所以問題變得複雜,主要就是需要考慮到網絡的延時和不可靠。。。一個大坑)

  • 分佈式鎖還是可以將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如Redis、Memcache。至於利用數據庫、文件等做鎖與單機的實現是一樣的,只要保證標記能互斥就行。

實現方式

  • 基於數據庫實現
  • 基於緩存實現(redis)
  • zookeeper實現

本篇基於redis實現分佈式鎖

實現的三個關鍵點

  • 原子性:加鎖和解鎖原子操作不可打斷
  • 避免死鎖:鎖不會被某個進程一直佔有或者在佔有鎖進程無法解鎖(宕機)情況下能夠解鎖
  • 互斥性:同一時刻只能有一個進程獲得鎖
  • 正確解鎖:鎖的佔有者只能解除自己的鎖

代碼實現(jedis)

redis配置

host=127.0.0.1
port=6379
password=123456

redis配置類

package com.zyl.redis.config;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@Configuration
@PropertySource(value = {"classpath:redis-conf.properties"})
public class RedisConfig
{
    @Value("${host}")
    private String host;

    @Value("${port}")
    private int port;

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

    @Bean
    public JedisPool getJedisPool()
    {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxIdle(50);
        config.setMaxTotal(120);
        config.setMinIdle(20);
        config.setMaxWaitMillis(10000);

        return new JedisPool(config, host, port, 10000, password);
    }

    @Bean
    public Jedis getJedis()
    {
        return getJedisPool().getResource();
    }
}

 

package com.zyl.redis.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.ArrayList;
import java.util.List;

@Component
public class Lock
{
    @Autowired
    private JedisPool jedisPool;

    //redis2.6支持lua腳本,實現操作原子性
    //鎖的內容和當前鎖的內容是否一致,否則不能解鎖表明已經被其它進程獲得
    private static final String unlockScript = "if (redis.call('exists',KEYS[1]) == 0 " +
            "or redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1]) else return -1 end";

    public void tryLock(String key, String value, int exSeconds)
    {
        //嘗試加鎖;加鎖失敗繼續嘗試
        while (!lock(key, value, exSeconds))
        {
            System.out.println(Thread.currentThread().getName() + " 正在獲取鎖");
        }
    }

    private boolean lock(String key, String value, int exSeconds)
    {
        //枷鎖
        SetParams setParams = new SetParams();
        //if not exists;key存在不做任何操作,否則設置
        setParams.nx();
        //設置過期時間,避免死鎖
        setParams.ex(exSeconds);

        Jedis jedis = jedisPool.getResource();
        String ret = jedis.set(key, value, setParams);
        //歸還鏈接,否則連接池鏈接一直被佔用,其它線程不能獲得鏈接
        if (null != jedis) jedis.close();
        if (!StringUtils.isEmpty(ret) && "OK".equals(ret))
        {
            System.out.println(Thread.currentThread().getName() + " 加鎖成功");
            return true;
        }

        System.out.println(Thread.currentThread().getName() + " 加鎖失敗");
        return false;
    }

    public void unLock(String key, String value)
    {
        List<String> keys = new ArrayList<>();
        List<String> values = new ArrayList<>();

        keys.add(key);
        values.add(value);

        Jedis jedis = jedisPool.getResource();
        //解鎖;執行lua腳本實現原子操作
        Object ret = jedis.eval(unlockScript, keys, values);
        if (null != jedis) jedis.close();
        if (-1 == ((Long)ret))
        {
            System.out.println(Thread.currentThread().getName() + " 解鎖失敗");
        }
        else
        {
            System.out.println(Thread.currentThread().getName() + " 解鎖成功:" + ret);
        }
    }
}

測試代碼

package com.zyl.redis.service;

import com.zyl.redis.lock.Lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class RedisLockTest
{
    private static final String LOCK_KEY = "TEST_LOCK";

    private static final int LOCK_EXPIRY_SECONDS = 100;

    //線程變量,容易實現了線程數據同步
    private ThreadLocal<String> local = new ThreadLocal<>();

    public static int count = 0;

    @Autowired
    private Lock lock;

    public void test()
    {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //爲鎖設置一個隨機變量與線程關聯起來;避免其它線程解鎖
                //例如:進程A獲得鎖之後由於業務複雜在過期時間內未完成沒有解鎖
                //此時鎖過期進程B獲得鎖;
                //進程A業務執行完畢去釋放鎖,此時通過這個隨機值判斷時候自己的鎖
                //不是就不會釋放鎖
                String uuid = UUID.randomUUID().toString();
                local.set(uuid);
                lock.tryLock(LOCK_KEY, local.get(), LOCK_EXPIRY_SECONDS);
                System.out.println(Thread.currentThread().getName() + " 運行: " + count++);
                lock.unLock(LOCK_KEY, local.get());
            }
        };

        Thread thread = null;
        for (int i = 0; i < 50; i++)
        {
            thread = new Thread(runnable, "Thread-" + i);
            thread.start();
        }
    }
}

測試截圖

maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zyl</groupId>
    <artifactId>redislock</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.0.1</version>
    </dependency>
    </dependencies>

</project>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章