Redis 分布式锁2

场景

一般电商网站都会遇到秒杀、特价之类的活动,大促活动有一个共同特点就是访问量激增,在高并发下会出现成千上万人抢购一个商品的场景。虽然在系统设计时会通过限流、异步、排队等方式优化,但整体的并发还是平时的数倍以上,参加活动的商品一般都是限量库存,如何防止库存超卖,避免并发问题呢?分布式锁就是一个解决方案。

“分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种

自己写一个简单的 redis分布式锁

加锁时

加锁时使用 set 命令,使用 加锁执行命令

SET resource_name random_value NX PX 30000

由于就一条命令,是原子性,比较安全。

SET 命令格式说明:
  SET key value [EX seconds] [PX milliseconds] [NX|XX]
示例:
  SET lock1 100 NX PX 30000
  这设置了一个  名字叫做 lock1 的锁;100标识随机数;NX 表示只在键不存在时,才对键进行设置操作;PX 和后面的数字表示过期时间。

这个随机数,由客户端生成,用来标识持有锁的人,在删除时只能由持有锁的人来删除。

解锁

所以在解锁之前先判断一下是不是自己加的锁,是自己加的锁再释放,不是就不释放。所以伪代码如下

if (random_value .equals(redisClient.get(resource_name))) {
  del(key)
}

因为判断和解锁是2个独立的操作,不具有原子性,所以解锁的过程要执行如下的Lua脚本,通过Lua脚本来保证判断和解锁具有原子性

if redis.call("exists",KEYS[1]) == 0 then
    return 0
end

if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end

Redis执行Lua脚本的命令,从Redis2.6开始,内嵌Lua环境,通过 EVAL 命令可以执行脚本

命令格式:
  EVAL script numkeys key [key...] arg [arg...]
参数说明:
  script 表示脚本内容。
  numkeys 表示 参数的个数
  key 表示 键
  arg 表示参数

问:在 LUA 脚本中如何掉 redis的 set get 等命令呢?
答:使用 redis.call(...)  方法来调用

用 java 代码实现 上锁

/**
    * 上锁
    *
    * @param key     锁名
    * @param uid     使用者的标识,可用随机数等
    * @param timeout 超时(毫秒)
    * @return
    */
   public boolean tryLock(String key, String uid, long timeout) {
       //System.out.printf("尝试获得锁%s, uid=%s \n", key, uid);
       // 等同于:SET lock1 100 NX PX 30000
       Boolean isok = stringRedisTemplate.opsForValue().setIfAbsent(
               LOCK_PREFIX + key,
               uid,
               timeout, TimeUnit.MILLISECONDS);
       //System.out.println("获得锁=" + isok);
       return isok != null && isok;
   }

用 java 代码实现 释放锁

先把上面的 解锁的lua 脚本放到一个文件 unlock.lua 里,放置到项目的资源文件夹中。

/**
  * 解锁
  * @param key
  * @param uid
  * @return
  */
 public Long unLock(String key, String uid) {
     //System.out.printf("尝试释放锁 %s ,uid=%s\n", key, uid);
     DefaultRedisScript defaultRedisScript = new DefaultRedisScript<>();
     defaultRedisScript.setResultType(Long.class);
     defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis_lock/unlock.lua")));
     Long execute = stringRedisTemplate.execute(defaultRedisScript, Arrays.asList(LOCK_PREFIX + key), uid);
     //System.out.println("释放锁=" + execute);
     return execute;
 }

最后

如果不用 Redisson,自己写得也能用,不够也有缺陷:
缺陷:

  • 会有业务未执行完,锁过期的问题,也就是锁不具有可重入性的特点。
  • 在尝试获取锁的时候,是非阻塞的,不满足在一定期限内不断尝试获取锁的场景。

以上两点,都可以采用 Redisson框架里的锁 解决

你可能感兴趣的:(Redis 分布式锁2)