当某个用户执行秒杀请求的时候,把秒杀请求把(秒杀请求地址)或者(给该秒杀起个统一的名字)作为key存入SETNX,value随便写,当做锁。
秒杀操作完成,把Redis的SETNX中它存的这个key删除掉。(锁过期的话也会自动删除)
Redis的SETNX命令非常适合排他性的锁实现。该命令只会在键不存在的情况下才会为键设置值,一个进程申明独占某个资源的方式就是对一个键使用SETNX,这样当别的进程再去使用这个命令设置这个键的时候就会失败进而无法获得锁,这样满足了我们的前言中锁的互斥性原则。这时我们的程序和我们的锁获得与释放可以这么写。
在此期间如果其他用户也执行该秒杀请求,首先会先去Redis中去查key是否已经存在,如果存在,当前线程无法执行秒杀请求,秒杀失败。
升级版分布式锁在简单分布式锁的基础上加一个随机码并假如超时时间,这个随机码作为value存入SETNX
- 使用SETNX实现排他性锁
- 使用超时限制特性来避免死锁
- 释放锁的时候需要进行检查来避免误释放别的进程的锁
因为简单的分布式锁存在这种情况,假如A用户点击秒杀按钮,把分布式锁加上了,然后突然出BUG卡了,或者无法继续执行下一步,此时B用户也点击了秒杀按钮,去Redis中查找对应的分布式锁,发现已经有人占用了,无法执行秒杀请求,秒杀失败,后续的用户也是如此。
还有一种情况是,A用户点击秒杀按钮,把分布式锁加上了,然后突然出BUG卡了,或者无法继续执行下一步,后来超时时间过了,分布式锁过期了,自动删除,此时B用户点击了秒杀按钮,去Redis中查找对应的分布式锁,发现已经没人占用,便在Redis存秒杀请求的key,加分布式锁,准备执行秒杀请求,此时A用户那边又突然好了,秒杀操作执行完了,把B用户的分布式锁给释放了,这就有问题了,可能会有C用户发现没人使用分布式锁,加锁,与B用户同时进行秒杀请求,出现问题。
所以需要加上随机码和超时时间,一来是为了防止某个用户请求执行过程中出bug卡在那里,后续的用户无法操作,二来是为了防止一个用户把别的用户的分布式锁释放了,造成后续的一系列问题。
但是这样的分布式锁还是不可行的,在海量请求下还有可能A用户执行秒杀请求,刚判断完分布式锁可用,锁过期了,自动删除,但是不影响A用户执行后续操作,B用户执行秒杀请求,判断分布式锁发现可用,B也执行后续操作,可以发现,AB同时执行后续操作,会出现问题。
如何解决?使用Redisson或者Lua脚本可以解决这个问题,Redis官方推荐Lua脚本,但是比较Lua比较复杂,
Redisson相对更好一些。
但是Redis实现的分布式锁还是存在问题的,因为Redis集群下,我们都知道主从复制,
但是Redis是只要主服务器更新完,不等剩下的从服务器同步更新,就直接告诉应用更新完成,
这就会出现问题,比如某个时刻,主服务器刚更新完,还没同步到从服务器,告知了应用更新完成,突然主服务器挂了,造成数据丢失,分布式锁没同步到从服务器,造成后续的一系列问题。
这也就是为什么不建议使用Redis分布式锁的原因。
Zookeeper可以解决这个问题,因为他是主从服务器都更新完毕,才告知应用更新完成。
上述是思想,以下是实现:具体实现参考文章:分布式锁看这篇就够了 - 知乎
基于 redis 的 setnx()、expire() 方法做分布式锁
setnx()
setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。
该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;
如果当前 key 已经存在,则设置当前 key 失败,返回 0。
expire()
expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,
只能通过 expire() 来对 key 设置。
使用步骤
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执行完业务代码后,可以通过 delete 命令删除 key。
这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。
比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,
那么就依然会出现死锁的问题,所以如果要对其进行完善的话,
可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。
基于 redis 的 setnx()、get()、getset()方法做分布式锁
这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。
getset()
这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,
并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:
getset(key, "value1") 返回 null 此时 key 的值会被设置为 value1getset(key, "value2") 返回 value1 此时 key 的值会被设置为 value2依次类推!
使用步骤
1、setnx(lockkey, 当前时间+过期超时时间),
如果返回 1,则获取锁成功;
如果返回 0 则没有获取到锁,转向 2。
2、get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,
如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
3、计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime)
会返回当前 lockkey 的值currentExpireTime。
4、判断 currentExpireTime 与 oldExpireTime 是否相等,
如果相等,说明当前 getset 设置成功,获取到了锁。
如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5、在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,
比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,
则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
import cn.com.tpig.cache.redis.RedisService;
import cn.com.tpig.utils.SpringUtils;
//redis分布式锁
public final class RedisLockUtil {
private static final int defaultExpire = 60;
private RedisLockUtil() {
//
}
public static boolean lock(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key, "1");
if(status == 1) {
redisService.expire(key, expire);
return true;
}
return false;
}
public static boolean lock(String key) {
return lock2(key, defaultExpire);
}
public static boolean lock2(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long value = System.currentTimeMillis() + expire;
long status = redisService.setnx(key, String.valueOf(value));
if(status == 1) {
return true;
}
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if(oldExpireTime < System.currentTimeMillis()) {
//超时
long newExpireTime = System.currentTimeMillis() + expire;
long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
if(currentExpireTime == oldExpireTime) {
return true;
}
}
return false;
}
public static void unLock1(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
redisService.del(key);
}
public static void unLock2(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if(oldExpireTime > System.currentTimeMillis()) {
redisService.del(key);
}
}
}
public void drawRedPacket(long userId) {
String key = "draw.redpacket.userid:" + userId;
boolean lock = RedisLockUtil.lock2(key, 60);
if(lock) {
try {
//领取操作
} finally {
//释放锁
RedisLockUtil.unLock(key);
}
} else {
new RuntimeException("重复领取奖励");
}
}



