场景
一般电商网站都会遇到秒杀、特价之类的活动,大促活动有一个共同特点就是访问量激增,在高并发下会出现成千上万人抢购一个商品的场景。虽然在系统设计时会通过限流、异步、排队等方式优化,但整体的并发还是平时的数倍以上,参加活动的商品一般都是限量库存,如何防止库存超卖,避免并发问题呢?分布式锁就是一个解决方案。
“分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于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框架里的锁 解决