分布式锁是什么?
互联网飞速发展,系统架构不断升级,从单体应用到集群。在单体应用当中,synchronized完全够用,但是集群下synchronized就无法起到作用了。这时分布式锁就出现了,分布式锁的本质和synchronized一致,都是为了多线程下一部分代码只能被一个线程执行。分布式锁市面上现在也有多种现有框架,实现方式有很多,实现原理大同小异。本篇文章将使用redis来实现分布式锁。
分布式锁实现原理
简单来说:
- A线程加锁就是往redis中存入一个key,执行完删除key
- B线程尝试获取锁redis存在当前key就证明锁目前有持有者;不存在则存入key获取锁成功
分布式锁实战
实现方式有很多。这里我们以Spring Boot为例,使用RedisTemplate实现。
测试用模拟下单扣减库存作为例子。
实现
代码结构图:
redis配置:
spring:
redis:
database: 1
host: localhost
port: 6379
password: 123456
timeout: 5000
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
redis工具类(关键)
重点只需要关注 lock 和 unLock 方法
@Component
@Slf4j
public class RedisUtil {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "lock_";
/**
* 获取锁。
* 该获取锁方法有如下特性:
* 1.如果获取锁成功,会设置锁的生存时间;
* 2.虽然大多数情况下redis的锁都有生存时间,
* 但是为了防止在上锁后、设置锁的生存周期
* 之前获取锁的方法出现了异常而终止。我们加入如下判断:
* 如果获取锁失败,会检查已存在锁是否设置有生存时间,
* 如果没有设置生存时间,那么会给锁设置生存时间。
*
* @param lockName 锁名称
* @return 如果获取锁成功则返回锁键对应值,否则返回null
*/
public String lock(String lockName) {
// 等待超时时间(可更灵活,这里为了简单理解,直接写死)
long waitTimeOut = 1000 * 30;
// 锁生存时间(可更灵活,这里为了简单理解,直接写死)
int lockTimeOut = 30;
String lockKey = LOCK_PREFIX + lockName;
String lockId = UUID.randomUUID().toString();
long end = System.currentTimeMillis() + waitTimeOut;
try {
// 等待锁未超时
while (System.currentTimeMillis() < end) {
if (setIfAbsent(lockKey, lockId)) {
// 成功获得锁 设置生存时间
expire(lockKey, lockTimeOut);
log.info("----------> 生成锁:" + lockId);
return lockId;
}
// 未获得锁
if (exists(lockKey) && notSetExpire(lockKey)) {
// 避免获取锁的方法出现了异常被终止导致死锁,如果没有设置生存时间,那么会给锁设置生存时间
expire(lockKey, lockTimeOut);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
log.error("lock error ------->", e);
unLock(lockKey, lockId);
}
// 等待锁超时
return null;
}
/**
* 解锁。
* 解锁时将判断锁键对应值是否是给定的值,防止误解锁。
*
* @param lockName 锁名称
* @param lockId 锁键对应值
* @return true如果解锁成功,否则返回false
*/
public void unLock(String lockName, String lockId) {
String lockKey = LOCK_PREFIX + lockName;
if (get(lockKey) != null && get(lockKey).equals(lockId)) {
// 删除锁
del(lockKey);
log.info("----------> 成功删除锁:" + lockId);
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
try {
return redisTemplate.opsForValue().get(key);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
/**
* setNX 不存在存入
*
* @param key
* @param value
* @return
*/
public boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 从redis中获取key对应的过期时间;
*
* @param key
* @return 如果该值有过期时间,就返回相应的过期时间;
* 如果该值没有设置过期时间,就返回-1;
* 如果没有该值,就返回-2;
*/
public long expire(String key) {
return redisTemplate.opsForValue().getOperations().getExpire(key);
}
/**
* 判断key是否过期
*
* @param key
* @return
*/
public boolean notSetExpire(String key) {
return expire(key) == -1;
}
/**
* 判断key是否存在
*
* @param key
* @return
*/
public boolean exists(String key) {
return redisTemplate.hasKey(key);
}
}
创建OrderService
public interface OrderService {
/**
* 生成订单
*/
void generateOrder();
}
创建OrderService实现类
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private RedisUtil redisUtil;
/**
* 模拟库存
*/
private int stock = 0;
/**
* 生成订单
*/
@Override
public void generateOrder() {
// 不加锁测试
deductStock();
/*
// 加锁测试
// .....
// 扣减库存
String lockKey = "lock_generateOrder";
String lockId = redisUtil.lock(lockKey);
if (Objects.isNull(lockId)) {
return;
}
try {
deductStock();
} catch (Exception e) {
log.error("generateOrder error", e);
} finally {
redisUtil.unLock(lockKey, lockId);
}
// .....
*/
}
private void deductStock() {
stock = --stock;
System.out.println(stock);
}
}
创建OrderController
@RestController
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping("/generateOrder")
public void generateOrder() {
orderService.generateOrder();
}
}
测试情况
这里测试只是为了证实锁是否有作用,测试例子并非符合实际业务场景。不用纠结,重点以理解分布式锁为主。
由于没有加分布式锁,并发下单1000次,最后输出的stock应该为-1000
测试实际输出:-925
这不是预期的结果。
将生成订单实现加锁:
/**
* 生成订单
*/
@Override
public void generateOrder() {
// 不加锁测试
// deductStock();
// 加锁测试
// .....
// 扣减库存
String lockKey = "lock_generateOrder";
String lockId = redisUtil.lock(lockKey);
if (Objects.isNull(lockId)) {
return;
}
try {
deductStock();
} catch (Exception e) {
log.error("generateOrder error", e);
} finally {
redisUtil.unLock(lockKey, lockId);
}
// .....
}
再次测试并发下单1000次,最后输出的stock为-1000
这就是我们预期的结果。
说明下,虽然测试用的单实例,集群下锁同样生效。虽然单实例下直接使用synchronized能达到更好的锁效果,但本文主要的目的是为了讲解分布式锁的实现。
分布式锁的实现方式有很多,理解思想使用任何中间件都能实现分布式锁。只是性能好不好的问题。