Redis分佈式鎖的實現(Jedis和Redisson兩個方案)

應用場景

    分佈式鎖主要用於解決,公司中不一樣業務系統對同一功能的數據產生髒讀或重複插入。html

    好比公司現有三個小組分別開發WAP站、小程序、APP客戶端,而這三個系統都存在領紅包功能。java

    業務要求每人每日只能領取一個紅包,若是有人同時登錄三個系統那麼就可以同一時間領取到三個紅包。git

分佈式鎖的要求

分佈式鎖要知足如下基本要求:github

  1. 共享鎖。多系統可以共享同一個鎖機制。
  2. 互斥性。在任意時刻,只有一個請求能持有鎖。
  3. 無死鎖。在程序崩潰時可以,自動釋放鎖。
  4. 持有者解鎖。鎖只能被加鎖的請求解鎖,其餘請求沒法解鎖。

Jedis實現分佈式鎖

本例參考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/redis

例子已上傳碼雲:https://gitee.com/imlichao/jedis-distributed-lock-examplespring

添加依賴

本例使用spring boot提供的redis實現,並無直接引入jedis依賴。這樣作的好處是,能夠在項目中同時使用Jedis和RedisTemplate實例。數據庫

pom.xml文件小程序

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

配置文件

application.properties文件緩存

#redis
spring.redis.database=0
spring.redis.host=18.6.8.22
spring.redis.password=Mfqy_redis_password_
spring.redis.port=6379
#鏈接超時時間(項目或鏈接池連接redis超時的時間)
spring.redis.timeout=2000
#最大鏈接數(建議爲 業務指望QPS/單個鏈接的QPS,50000/1000=50)
spring.redis.pool.max-active=50
#最大空閒連接數(爲減少伸縮產生的性能消耗,建議和最大鏈接數設成一致的)
spring.redis.pool.max-idle=50
#最小空閒鏈接數(0表明在無請求的情況下從不建立連接)
spring.redis.pool.min-idle=0
#鏈接池佔滿後沒法獲取鏈接時的阻塞時間(超時後拋出異常)
spring.redis.pool.max-wait=3000

Jedis工廠類

因爲咱們使用了spring boot提供的redis實現,因此咱們不能直接獲取到jedis對象。Jedis工廠類從RedisConnectionFactory中獲取Redis鏈接(JedisConnection實現類),而後使用反射的方法從中取得了Jedis實例。服務器

/**
 * Jedis工廠類(單例模式)
 */
@Service
public class JedisFactory {
    @Autowired
    private RedisConnectionFactory connectionFactory;

    private JedisFactory(){}

    private static Jedis jedis;
    /**
     *  得到jedis對象
     */
    public Jedis getJedis() {
        //從RedisConnectionFactory中獲取Redis鏈接(JedisConnection實現類),而後使用反射的方法從中取得了Jedis實例
        if(jedis == null){
            Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
            ReflectionUtils.makeAccessible(jedisField);
            jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());
        }
        return jedis;
    }
}

爲避免死鎖的發生,加鎖和設定失效時間必須是一個原子性操做。不然一旦在加鎖後程序出錯,沒可以執行設置失效時間的方法時,就會產生死鎖。 可是RedisTemplate屏蔽了插入數據和設置失效時間同時執行的方法,咱們只能獲取到Jedis實例來執行。

分佈式鎖實現類

分佈式鎖主要實現了兩個方法即佔用鎖和釋放鎖。

這裏須要注意佔用鎖和釋放鎖都要保證原子性操做,避免程序異常時產生死鎖。

鎖id主要用於標識持有鎖的請求,在釋放瑣時用來判斷只有持有正確鎖id的請求才能執行解鎖操做。

/**
 * redis分佈式鎖
 */
@Service
public class DistributedLock {

    @Autowired
    private JedisFactory JedisFactory;

    /**
     * 佔用鎖
     * @param lockKey 鎖key
     * @return 鎖id
     */
    public String occupyDistributedLock(String lockKey) {
        //得到jedis實例
        Jedis jedis = JedisFactory.getJedis();
        //鎖id(必須擁有此id才能釋放鎖)
        String lockId = UUID.randomUUID().toString();
        //佔用鎖同時設置失效時間
        String isSuccees = jedis.set(lockKey, lockId, "NX","PX", 15000);
        //佔用鎖成功返回鎖id,不然返回null
        if("OK".equals(isSuccees)){
            return lockId;
        }else{
            return null;
        }
    }

    /**
     * 釋放鎖
     * @param lockKey 鎖key
     * @param lockId 加鎖id
     */
    public void releaseDistributedLock(String lockKey,String lockId) {
        if(lockId != null){
            //得到jedis實例
            Jedis jedis = JedisFactory.getJedis();
            //執行Lua代碼刪除lockId匹配的鎖
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId));
        }
    }
}

解釋一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法

格式 - String set(String key, String value, String nxxx, String expx, long time);
功能 - 存儲數據到緩存中,並制定過時時間和當Key存在時是否覆蓋。
參數 - 
key :redis key
value : redis值
nxxx: 只能取NX或者XX,若是取NX,則只有當key不存在是才進行set,若是取XX,則只有當key已經存在時才進行set
expx: 只能取EX或者PX,表明數據過時時間的單位,EX表明秒,PX表明毫秒。
time: 過時時間,單位是expx所表明的單位。

測試代碼

Controller

/**
 * 分佈式鎖測試類
 */
@Controller
public class DistributedLockController {
    @Autowired
    private DistributedLock distributedLock;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){
        String key = "userid:55689";

        String lockId = null;
        try{
            //佔用鎖
            lockId = distributedLock.occupyDistributedLock(key);
            if(lockId != null){
                //程序執行
                TimeUnit.SECONDS.sleep(10);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //釋放鎖
            distributedLock.releaseDistributedLock(key,lockId);
        }
        redirectAttributes.addFlashAttribute("lockId",lockId);

        return "redirect:/";
    }
}

頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分佈式鎖測試</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">佔用鎖</button>
<br/>
<!-- 佔用成功返回鎖id -->
<#if lockId??>${lockId}</#if>
</body>
</html>

Redisson實現分佈式鎖(推薦)

使用Redisson提供的分佈式鎖更加方便,並且鎖的具體細節也不須要考慮。

例子已上傳碼雲:https://gitee.com/imlichao/redisson-distributed-lock-example

官網:https://redisson.org/

文檔:https://github.com/redisson/redisson/wiki

SpringBoot配置

添加依賴

spring boot 中引用專用依賴,會自動生成配置和spring bean的實例。

pom.xml文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.10.1</version>
</dependency>

配置文件

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

測試代碼

Controller

/**
 * 分佈式鎖測試類
 */
@Controller
public class DistributedLockController {
    @Autowired
    private RedissonClient redisson ;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes){
        RLock lock = null;
        try{
            //鎖的key
            String key = "MF:DISTRIBUTEDLOCK:S:personId_1001";
            //得到分佈式鎖實例
            lock = redisson.getLock(key);
            //加鎖而且設置自動失效時間15秒
            lock.lock(15, TimeUnit.SECONDS);
            //程序執行
            TimeUnit.SECONDS.sleep(10);
            //獲取網絡時間(多服務器測試統一時間)
            URL url=new URL("http://www.baidu.com");
            URLConnection conn=url.openConnection();
            conn.connect();
            long dateL=conn.getDate();
            Date date=new Date(dateL);
            //打印和返回結果
            System.out.println(date);
            redirectAttributes.addFlashAttribute("success",date);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //釋放鎖
            if (lock != null) lock.unlock();
        }

        return "redirect:/";
    }
}

頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分佈式鎖測試</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">佔用鎖</button>
<br/>
<#if success??>${success?datetime}</#if>
</body>
</html>

手動配置

添加依賴

pom.xml文件

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.10.1</version>
</dependency>

配置文件

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

配置類

/**
 * Redisson配置
 */
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.host}")
    private String host;

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

    @Value("${spring.redis.port}")
    private String port;

    @Bean
    RedissonClient createConfig() {
        Config config = new Config();
        //設置編碼方式爲 Jackson JSON 編碼(不設置默認也是這個)
        config.setCodec(new JsonJacksonCodec());
        //雲託管模式設置(咱們公司用的阿里雲redis產品)
        config.useReplicatedServers()
                //節點地址設置
                .addNodeAddress("redis://"+host+":"+port)
                //密碼
                .setPassword(password)
                //數據庫編號(默認0)
                .setDatabase(database);

        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

測試代碼與SpringBoot配置同樣

相關文章
相關標籤/搜索