首頁 > 軟體

Spring Boot 實現Redis分散式鎖原理

2022-08-04 14:00:24

分散式鎖實現

引入jar包

<dependency>
 <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
   <exclusion>
 <groupId>io.lettuce</groupId>
 <artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
 <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
 </dependency>

說明:本文采用jedis來實現分散式鎖。

封裝工具類

@Component
public class RedisLockUtil
{
    private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 加鎖方法僅針對單範例 Redis,哨兵、叢集模式無法使用
     *
     * @param lockKey 加鎖鍵
     * @param clientId 加鎖使用者端唯一標識(採用UUID)
     * @param seconds 鎖過期時間
     * @return true標識加鎖成功、false代表加鎖失敗
     */
    public Boolean tryLock(String lockKey, String clientId, long seconds)
    {
        try
        {
            return redisTemplate
                    .execute((RedisCallback<Boolean>) redisConnection -> {
                        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                        SetParams params =new SetParams();
                        params.nx();
                        params.px(seconds);
                        String result = jedis.set(lockKey, clientId, params);
                        if (LOCK_SUCCESS.equals(result))
                        {
                            return Boolean.TRUE;
                        }
                        return Boolean.FALSE;
                    });
        }
        catch (Exception e)
        {
            logger.error("tryLock error",e);
        }

        return false;
    }
    /**
     *釋放鎖,保持原子性操作,採用了lua指令碼
     *
     * @param lockKey
     * @param clientId
     * @return
     */
    public Boolean unLock(String lockKey, String clientId)
    {
        try
        {
            return  redisTemplate
                    .execute((RedisCallback<Boolean>) redisConnection -> {
                        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                        Object result = jedis.eval(RELEASE_LOCK_SCRIPT,
                                Collections.singletonList(lockKey),
                                Collections.singletonList(clientId));
                        if (RELEASE_SUCCESS.equals(result))
                        {
                            return Boolean.TRUE;
                        }
                        return Boolean.FALSE;
                    });
        }
        catch (Exception e)
        {
            logger.error("unlock error",e);
        }
        return Boolean.FALSE;
    }
}

說明:加鎖的原理是基於Redis的NX、PX命令,而解鎖採用的是lua指令碼實現。

模擬秒殺扣減庫存

public int lockStock()
    {
        String lockKey="lock:stock";
        String clientId = UUID.randomUUID().toString();
        long seconds =1000l;

        try
        {
            //加鎖
            boolean flag=redisLockUtil.tryLock(lockKey, clientId, seconds);
            //加鎖成功
            if(flag)
            {
               logger.info("加鎖成功 clientId:{}",clientId);
               int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock"));
               if(stockNum>0)
               {
                  stockNum--;
                  redisUtil.set("seckill:goods:stock",String.valueOf(stockNum));
                  logger.info("秒殺成功,剩餘庫存:{}",stockNum);
               }
               else
               {
                  logger.error("秒殺失敗,剩餘庫存:{}", stockNum);
               }
               //獲取庫存數量
               return stockNum;
            }
            else
            {
                logger.error("加鎖失敗:clientId:{}",clientId);
            }
        }
        catch (Exception e)
        {
           logger.error("decry stock eror",e);
        }
        finally
        {
           redisLockUtil.unLock(lockKey, clientId);
        }
        return 0;
    }

測試程式碼

@RequestMapping("/redisLockTest")
    public void redisLockTest()
    {
        // 初始化秒殺庫存數量
        redisUtil.set("seckill:goods:stock", "10");

        List<Future> futureList = new ArrayList<>();

        //多執行緒非同步執行
        ExecutorService executors = Executors.newScheduledThreadPool(10);
        //
        for (int i = 0; i < 30; i++)
        {
            futureList.add(executors.submit(this::lockStock));

            try
            {
               Thread.sleep(100);
            }
            catch (InterruptedException e) 
            {
               logger.error("redisLockTest error",e);
            }
        }

        // 等待結果,防止主執行緒退出
        futureList.forEach(t -> {
            try 
            {
                int stockNum =(int) t.get();
                logger.info("庫存剩餘數量:{}",stockNum);
            }
            catch (Exception e)
            {
               logger.error("get stock num error",e);
            }
        });
    }

執行結果如下:

方案優化

上述分散式鎖實現庫存扣減是否存在相關問題呢?

問題1:扣減庫存邏輯無法保證原子性,

具體的程式碼如下:

int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock"));
if(stockNum>0)
 {
    stockNum--;
    redisUtil.set("seckill:goods:stock",String.valueOf(stockNum));
 }

這是典型的RMW模型,前面章節已經介紹了具體的實現方案,可以採用lua指令碼和Redis的incry原子性命令實現,這裡採用lua指令碼來實現原子性的庫存扣減。

具體實現如下:

  public long surplusStock(String key ,int num)
   {
       StringBuilder lua_surplusStock = new StringBuilder();
       lua_surplusStock.append("   local key = KEYS[1];");
       lua_surplusStock.append("   local subNum = tonumber(ARGV[1]);");
       lua_surplusStock.append("   local surplusStock=tonumber(redis.call('get',key));");
       lua_surplusStock.append("    if (surplusStock- subNum>= -1) then");
       lua_surplusStock.append("        return redis.call('incrby', KEYS[1], 0-subNum);");
       lua_surplusStock.append("    else ");
       lua_surplusStock.append("    return -1;");
       lua_surplusStock.append("    end");
       
       List<String> keys = new ArrayList<>();
       keys.add(key);
       // 指令碼裡的ARGV引數
       List<String> args = new ArrayList<>();
       args.add(Integer.toString(num));

       long result = redisTemplate.execute(new RedisCallback<Long>() {
           @Override
           public Long doInRedis(RedisConnection connection) throws DataAccessException {
               Object nativeConnection = connection.getNativeConnection();
               // 單機模式
               if (nativeConnection instanceof Jedis) 
               {
                   return (Long) ((Jedis) nativeConnection).eval(lua_surplusStock.toString(), keys, args);
               }
               return -1l;
           }
       });
       return result;
   }

問題2:如果加鎖失敗,則會直接存取,無法重入鎖

因為單機版本的鎖是無法重入鎖,所以加鎖失敗就直接返回,此問題的解決方案,可以採用Redisson來實現,關於Redisson實現分散式鎖,將在後續的文章中進行詳細的講解。

總結

本文主要講解了Spring Boot整合Redis實現單機版本分散式鎖,雖然單機版分散式鎖存在鎖的續期、鎖的重入問題,但是我們還是需要掌握其原理和實現方法

到此這篇關於Spring Boot 實現Redis分散式鎖原理的文章就介紹到這了,更多相關Spring Boot Redis分散式鎖內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com