Redis 分布式锁2

2022-09-29 12:23:09 浏览数 (1)

场景

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

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

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

加锁时

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

代码语言:javascript复制
SET resource_name random_value NX PX 30000

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

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

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

解锁

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

代码语言:javascript复制
if (random_value .equals(redisClient.get(resource_name))) {
  del(key)
}

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

代码语言:javascript复制
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 命令可以执行脚本

代码语言:javascript复制
命令格式:
  EVAL script numkeys key [key...] arg [arg...]
参数说明:
  script 表示脚本内容。
  numkeys 表示 参数的个数
  key 表示 键
  arg 表示参数

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

用 java 代码实现 上锁

代码语言:javascript复制
/**
    * 上锁
    *
    * @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 里,放置到项目的资源文件夹中。

代码语言:javascript复制
/**
  * 解锁
  * @param key
  * @param uid
  * @return
  */
 public Long unLock(String key, String uid) {
     //System.out.printf("尝试释放锁 %s ,uid=%sn", key, uid);
     DefaultRedisScript<Long> 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框架里的锁 解决

0 人点赞