这是我参与更文挑战的第29天,活动详情查看: 更文挑战
什么是分布式锁?
首先要提到与分布式锁相对应的是线程锁、进程锁。
线程锁:主要给方法和代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:多个进程不在同一个系统,用分布式锁控制多个进程对资源的访问。
分布式锁要求:
高性能,高可用,避免死锁
分布式锁实现:
1基于数据库实现分布式锁
2基于缓存(Redis等)实现分布式锁
3基于Zookeeper实现分布式锁
实现加锁解锁
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RedisLockHelper {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
*
* @param targetId targetId - 商品的唯一标志
* @param timeStamp 当前时间+超时时间 也就是时间戳
* @return
*/
public boolean lock(String targetId, String timeStamp) {
if (stringRedisTemplate.opsForValue().setIfAbsent(targetId, timeStamp, 10, TimeUnit.SECONDS)) {
// 对应setnx命令,可以成功设置,也就是key不存在
return true;
}
// 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentLock = stringRedisTemplate.opsForValue().get(targetId);
// 如果锁过期 currentLock不为空且小于当前时间
if (!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
// 获取上一个锁的时间value 对应getset,如果lock存在
String preLock = stringRedisTemplate.opsForValue().getAndSet(targetId, timeStamp);
// 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
// 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if (!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock)) {
// preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
return false;
}
/**
* 解锁
*
* @param target
* @param timeStamp
*/
public void unlock(String target, String timeStamp) {
try {
String currentValue = stringRedisTemplate.opsForValue().get(target);
if (!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp)) {
// 删除锁状态
stringRedisTemplate.opsForValue().getOperations().delete(target);
}
} catch (Exception e) {
log.error("警报!警报!警报!解锁异常{}", e);
}
}
}
测试
import com.redis.demo.util.RedisLockHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RedisController {
@Autowired
RedisLockHelper redisLockHelper;
int surplusCount = 2;
/**
* 超时时间 5s
*/
private static final int TIMEOUT = 5*1000;
@RequestMapping(value = "/seckilling")
public String Seckilling(String targetId){
//加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLockHelper.lock(targetId,String.valueOf(time))){
return "排队人数太多,请稍后再试.";
}
// int surplusCount = 1;
// 查询该商品库存,为0则活动结束 e.g. getStockByTargetId
if(surplusCount==0){
return "活动结束.";
}else {
// 下单 e.g. buyStockByTargetId
//减库存 不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
surplusCount =surplusCount-1;
try{
Thread.sleep(200);//模拟减库存的处理时间
}catch (InterruptedException e){
e.printStackTrace();
}
// 减库存操作数据库 e.g. updateStockByTargetId
// buyStockByTargetId 和 updateStockByTargetId 可以同步完成(或者事物),保证原子性。
}
//解锁
redisLockHelper.unlock(targetId,String.valueOf(time));
return "恭喜您,秒杀成功。";
}
}
以上加锁方法适用于redis单实例,主备等 ,不适合redis集群