什么是分布式锁?
即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。
分布式锁应该具备哪些条件?
1:在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
2:高可用的获取锁与释放锁
3:高性能的获取锁与释放锁
4:具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
5:具备锁失效机制,即自动解锁,防止死锁
分布式锁的实现方式有那些?
1.使用关系型mysql数据库实现分布式锁。
2.使用redis非关系型数据库实现分布式锁。
3.使用zookeeper注册中心来实现分布式锁。
本文将详细介绍重要的分布式锁–给予redis的分布式锁。
A:Redisson 实现的分布式锁使用演示
B:自己实现的 Redis 分布式锁使用演示
数据库脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for product_stock
-- ----------------------------
DROP TABLE IF EXISTS `product_stock`;
CREATE TABLE `product_stock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`stock` int(11) NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '产品库存表\n' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of product_stock
-- ----------------------------
INSERT INTO `product_stock` VALUES (1, 20);
SET FOREIGN_KEY_CHECKS = 1;
yml配置信息
spring:
application:
name: lock-redis
# 数据库链接 要改成自己的数据链接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_URL:127.0.0.1}:3306/lock-test?autoReconnect=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
username: root
password: ${MYSQL_PASSWORD:abc123}
hikari:
max-lifetime: 500000
# redis链接,要改成自己的链接配置
redis:
host: ${REDIS_URL:127.0.0.1}
port: 6379
password: ${REDIS_PASSWORD:abc123}
database: 11
自己实现的分布式锁的方式。
/**
* 模拟减库存操作 - 自己实现 redis 锁接口
*
* @return str
*/
@GetMapping("/reduceStockByMyLock/{id}")
public String reduceStockByMyLock(@PathVariable("id") Integer id) {
return productStockService.reduceStockByMyLock(id);
}
@SneakyThrows
@Override
public String reduceStockByMyLock(Integer id) {
// requestId 确保每一个请求生成的都不一样,这里使用 uuid,也可以使用其他分布式唯一 id 方案
String requestId = UUID.randomUUID().toString().replace("-", "");
int expireTime = 10;
bulkRedisLock.lock(requestId, expireTime);
// 开启续命线程,
Thread watchDog = bulkRedisLock.watchDog(expireTime, requestId);
watchDog.setDaemon(true);
watchDog.start();
try {
ProductStock stock = productStockMapper.selectById(id);
if (stock != null && stock.getStock() > 0) {
productStockMapper.reduceStock(id);
} else {
throw new RuntimeException("库存不足!");
}
} finally {
watchDog.interrupt();
bulkRedisLock.unlock(requestId);
}
return "ok";
}
自己实现的配置锁BulkRedisLock
@Slf4j
@Component
@SuppressWarnings("all")
public class BulkRedisLock {
private static final String LOCK_PREFIX = "redisLock";
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 尝试获取锁
*
* @param requestId 请求id
* @param expireTime 过期时间 单位毫秒
* @return true false
*/
public boolean lock(String requestId, int expireTime) {
// 也可以使用 lua 脚本 "return redis.call('set',KEYS[1], ARGV[1],'NX','PX',ARGV[2])"
// 使用redis保证原子操作(判断是否存在,添加key,设置过期时间)
while (true) {
if (Boolean.TRUE.equals(stringRedisTemplate.boundValueOps(LOCK_PREFIX).
setIfAbsent(requestId, expireTime, TimeUnit.SECONDS))) {
return true;
}
}
}
/**
* 将锁释放掉
* <p>
* 为何解锁需要校验 requestId 因为不是自己的锁不能释放
* 客户端A加锁,一段时间之后客户端A解锁,在执行 lock 之前,锁突然过期了。
* 此时客户端B尝试加锁成功,然后客户端A再执行 unlock 方法,则将客户端B的锁给解除了。
*
* @param requestId 请求id
* @return true false
*/
public boolean unlock(String requestId) {
// 这里使用Lua脚本保证原子性操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long res = stringRedisTemplate.execute(redisScript, Collections.singletonList(LOCK_PREFIX), requestId);
return new Long(1).equals(res);
}
/**
* 创建续命子线程
*
* @param time 操作预期耗时
* @param requestId 唯一标识
* @return 续命线程 Thread
*/
public Thread watchDog(int time, String requestId) {
return new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(time * 2 / 3);
//重置时间
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1],ARGV[2]) " +
"else return '0' end";
List<Object> args = new ArrayList();
args.add(requestId);
args.add(time);
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
stringRedisTemplate.execute(redisScript, Collections.singletonList(LOCK_PREFIX), args);
} catch (Exception e) {
// sleep interrupted 是因为 sleep
// log.info("watchDog异常:{}", e.getMessage());
return;
}
}
});
}
}
mapper的xml文件
<update id="reduceStock">
update product_stock
set stock = stock - 1
where id = #{id}
</update>
控制层入库contoller
/**
* 模拟减库存操作 - 自己实现 redis 锁接口
*
* @return str
*/
@GetMapping("/reduceStockByMyLock/{id}")
public String reduceStockByMyLock(@PathVariable("id") Integer id) {
return productStockService.reduceStockByMyLock(id);
}
使用edisson 实现的分布式锁使用
控制controller
/**
* 模拟减库存操作 - redisson 实现
*
* @return str
*/
@GetMapping("/reduceStock/{id}")
public String reduceStockByRedisson(@PathVariable("id") Integer id) {
return productStockService.reduceStock(id);
}
业务实现
@Override
public String reduceStock(Integer id) {
RLock lock = redissonClient.getLock("lock");
lock.lock();
try {
ProductStock stock = productStockMapper.selectById(id);
if (stock != null && stock.getStock() > 0) {
productStockMapper.reduceStock(id);
} else {
throw new RuntimeException("库存不足!");
}
} finally {
lock.unlock();
}
return "ok";
}
以上的是分布式锁之-redis 若需完整代码 可识别二维码后 给您发代码。
若友友们有更好的分布式锁的实现方式 请在评论区留下你可贵的分享 谢谢!!!!