利用SETNX+EXPIRE实现分布式锁。
SETNX:如果key不存在,那么就新增一个<key,value>,如果key存在,则不做任何操作。
EXPIRE:为了防止网络拥塞,A拿到锁以后迟迟无法释放,故设置超时时间。防止B一直等待该key,A迟迟无法释放而出现死锁。
思路:
1.给userId+itemId形成的组合key设置一个value,设置成功就设置一个超时时间。
2.如果设置key成功,修改itemKill表(商品表),生成订单表itemKillSuccess
3.释放锁。(也就是删除userId+itemId形成的组合key)
下面是单节点redis:
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 商品秒杀核心业务逻辑的处理-redis的分布式锁
* @param killId
* @param userId
* @return
* @throws Exception
*/
@Override
public Boolean killItemV3(Integer killId, Integer userId) throws Exception {
Boolean result=false;
//订单上没有该UserId和itemID的行(该用户没有秒杀过该商品)
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
//TODO:借助Redis的原子操作实现分布式锁-对共享操作-资源进行控制
ValueOperations valueOperations=stringRedisTemplate.opsForValue();
final String key=new StringBuffer().append(killId).append(userId).append("-RedisLock").toString();//唯一的key
final String value=RandomUtil.generateOrderCode();//value并没有什么实际含义
Boolean cacheRes=valueOperations.setIfAbsent(key,value); //luna脚本提供“分布式锁服务”,SETNX和EXPIRE写在一起
//TOOD:遗留问题:此时redis部署节点宕机了
if (cacheRes){
stringRedisTemplate.expire(key,30, TimeUnit.SECONDS);//为了防止网络拥塞,A拿到锁以后,B一直等待的问题,我们就设置了一个超时时间。
try {
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);//根据商品killId,从itemKill表中找到可以秒杀商品的详情
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){ //商品存在,是秒杀商品,商品数量大于0
int res=itemKillMapper.updateKillItemV2(killId);//更新秒杀商品表
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);//生产订单
result=true;
}
}
}catch (Exception e){
throw new Exception("还没到抢购日期、已过了抢购时间或已被抢购完毕!");
}finally {
//根据key找到对应的value,然后释放锁
if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}
}
}
}else{
throw new Exception("Redis-您已经抢购过该商品了!");
}
return result;
}
上面代码有一个遗留问题:如果在设置key成功以后,还没有设置超时时间,redis宕机了,那么是不是也会出现死锁?
一种是luna脚本将SETNX和EXPIRE写在一起。
另一种是分布式锁。
下面是一个分布式锁:
redissonClient直接提供了tryLock。使用和java JDK的锁类似。
@Autowired
private RedissonClient redissonClient;
/**
* 商品秒杀核心业务逻辑的处理-redisson的分布式锁
* @param killId
* @param userId
* @return
* @throws Exception
*/
@Override
public Boolean killItemV4(Integer killId, Integer userId) throws Exception {
Boolean result=false;
final String lockKey=new StringBuffer().append(killId).append(userId).append("-RedissonLock").toString();
RLock lock=redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待30s,10秒后释放锁
Boolean cacheRes=lock.tryLock(30,10,TimeUnit.SECONDS);
if (cacheRes){
//TODO:核心业务逻辑的处理
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
int res=itemKillMapper.updateKillItemV2(killId);
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);
result=true;
}
}
}else{
throw new Exception("redisson-您已经抢购过该商品了!");
}
}
}finally {
lock.unlock();
//lock.forceUnlock();
}
return result;
}
下面直接使用zookeeper系统的分布式锁实现上面的业务:
手动实现一个zookeeper分布式锁参考:https://blog.csdn.net/qq_35688140/article/details/89480960
@Autowired
private CuratorFramework curatorFramework;
private static final String pathPrefix="/kill/zkLock/";
/**
* 商品秒杀核心业务逻辑的处理-基于ZooKeeper的分布式锁
* @param killId
* @param userId
* @return
* @throws Exception
*/
@Override
public Boolean killItemV5(Integer killId, Integer userId) throws Exception {
Boolean result=false;
InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");
try {
if (mutex.acquire(10L,TimeUnit.SECONDS)){
//TODO:核心业务逻辑
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
int res=itemKillMapper.updateKillItemV2(killId);
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);
result=true;
}
}
}else{
throw new Exception("zookeeper-您已经抢购过该商品了!");
}
}
}catch (Exception e){
throw new Exception("还没到抢购日期、已过了抢购时间或已被抢购完毕!");
}finally {
if (mutex!=null){
mutex.release();
}
}
return result;
}
zookeeper实现分布式锁和乐观锁redis实现分布式锁的比较:如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁
zookeeper实现分布式锁和悲观锁redis实现分布式锁的比较:如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁