Spring Boot 实现Redis分布式锁

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

前言

Redis分布式锁的原理已经在前面几章已经进行详细的讲解,掌握了相关理论知识后,我们还需要掌握其具体实现的方法,本文将Spring Boot集成Redis如何实现单机Redis分布式锁进行详细讲解。

分布式锁实现

引入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);
            }
        });
    }
复制代码

执行结果如下:

图片.png

方案优化

上述分布式锁实现库存扣减是否存在相关问题呢?

问题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实现单机版本分布式锁,虽然单机版分布式锁存在锁的续期、锁的重入问题,但是我们还是需要掌握其原理和实现方法,如有疑问,请随时反馈,大家共同学习,共同进步。

猜你喜欢

转载自juejin.im/post/7127667756693979173