分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。
复习一下set
不可以用setnx作为大厂的分布式锁!!!!
创建工程 redis_distributed_lock2
POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.10version>
<relativePath/>
parent>
<groupId>com.xfcygroupId>
<artifactId>redis_distributed_lock2artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>redis_distributed_lock2name>
<description>redis_distributed_lock2description>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<lombok.version>1.16.18lombok.version>
properties>
<dependencies>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.8version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
YML
server.port=7777
spring.application.name=redis_distributed_lock2
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.238.111
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
主启动类
@SpringBootApplication
public class RedisDistributedLock2Application {
public static void main(String[] args) {
SpringApplication.run(RedisDistributedLock2Application.class, args);
}
}
业务类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Value("${swagger2.enabled}")
private Boolean enabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.xfcy"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
.description("springboot+redis整合")
.version("1.0")
.termsOfServiceUrl("https://www.baidu.com/")
.build();
}
}
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
lock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
import com.xfcy.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(tags = "redis分布式锁测试")
public class InvetoryController {
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存sale,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
@ApiOperation("扣减库存saleByRedisson,一次卖一个")
@GetMapping(value = "/inventory/saleByRedisson")
public String saleByRedisson()
{
return inventoryService.saleByRedisson();
}
}
InventoryService 实现的一个简单版本的 分布式锁
import cn.hutool.core.util.IdUtil;
import com.xfcy.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
/**
* v6.0 使用 Lua 脚本 将 final的判断 + del 弄成原子操作
* 问题: 兼顾锁的可重入性
* 但是基本 v6.0 版本已经够用
* @return
*/
public String sale() {
String retMessage = "";
String key = "xfcyRedisLock";
String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// 暂停20毫秒,进行递归重试
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
// 改进点,修改为Lua脚本的Redis分布式锁调用,必须保证原子性
String luaScript =
"if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), value);
}
return retMessage + "\t" + "服务端口号:" + port;
}
/**
* v5.0 存在问题: final的判断 + del 不是一行原子操作,需要lua脚本进行修改
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// }finally {
// // 改进点,只能删除自己的key,而不是别的客户端
// // 问题:不能保证原子操作
// if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
// stringRedisTemplate.delete(key);
// }
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
/**
* v4.0 加了过期时间 stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)
* 问题:误删锁,如果线程A运行时间超出了过期时间
* 在线程A运行时,xfcyRedisLock这个key过期,另一个线程B进来加了key
* 线程A结束后,把线程B的锁删了
* stringRedisTemplate.delete(key); 只能自己删除自己的,需要添加判断是否是自己的锁
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// // 无法保证原子性
stringRedisTemplate.expire(key, 30L, TimeUnit.SECONDS);
//
// // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// }finally {
// stringRedisTemplate.delete(key);
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
/**
* 3.2版 setnx 用while判断
* 问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
* 不是程序出了问题,如果程序问题,最后还是会执行finally
* 没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// }finally {
// stringRedisTemplate.delete(key);
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
/**
* 3.1版 setnx 递归调用 容易导致 StackOverflowError
* 高并发唤醒后推荐用while判断而不是if
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
// // flag = false 抢不到的线程要继续重试 。。。
// if (!flag) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// sale();
// } else {
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// } finally {
// stringRedisTemplate.delete(key);
// }
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
/**
* v2.0 单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
*/
// private Lock lock = new ReentrantLock();
//
// public String sale() {
// String retMessage = "";
// lock.lock();
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// } finally {
// lock.unlock();
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
}
v 8.0 其实面对不是特别高的并发场景足够用了,单机redis也够用了
InventoryService
@Autowired
private DistributedLockFactory distributedLockFactory;
/**
* v8.0 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
*
* @return
*/
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// 演示自动续期的的功能
// try {
// TimeUnit.SECONDS.sleep(120);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
/**
* v7.0 兼顾锁的可重入性 setnx不满足了,需要hash结构的hset
* 上锁和解锁都用 Lua 脚本实现原子性
* 引入工厂模式 DistributedLockFactory 实现Lock接口 ,实现 redis的可重入锁
*
* @return
*/
// //private Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "xfcyRedisLock");
//
// public String sale() {
// String retMessage = "";
//
// Lock redisLock = distributedLockFactory.getDistributedLock("redis");
// redisLock.lock();
//
// //redisDistributedLock.lock();
// try {
// //1 查询库存信息
// String result = stringRedisTemplate.opsForValue().get("inventory001");
// //2 判断库存是否足够
// Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// //3 扣减库存
// if (inventoryNumber > 0) {
// stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
// retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
// System.out.println(retMessage);
//
// // 测试可重入性
// //testReEntry();
//
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// } finally {
// redisLock.unlock();
// //redisDistributedLock.unlock();
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
//
// private void testReEntry() {
// Lock redisLock = distributedLockFactory.getDistributedLock("redis");
// redisLock.lock();
//
// //redisDistributedLock.lock();
// try {
// System.out.println("测试可重入锁");
// } finally {
// redisLock.unlock();
// //redisDistributedLock.unlock();
// }
// }
mylock/DistributedLockFactory
package com.xfcy.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory() {
this.uuidValue = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType) {
if (lockType == null) {
return null;
}
if (lockType.equalsIgnoreCase("REDIS")) {
this.lockName = "xfcyRedisLock";
return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue);
}else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "xfcyZookeeperLock";
// TODO zoookeeper 版本的分布式锁
return null;
}else if (lockType.equalsIgnoreCase("MYSQL")){
this.lockName = "xfcyMysqlLock";
// TODO MYSQL 版本的分布式锁
return null;
}
return null;
}
}
mylock/RedisDistributedLock
package com.xfcy.mylock;
import cn.hutool.core.util.IdUtil;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
// @Component 引入DistributedLockFactory工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName; // KEYS[1]
private String uuidValue; // ARGV[1]
private long expireTime; // ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName = " + lockName +"\t" + "uuidValue = " + uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
// 暂停 60ms
Thread.sleep(60);
}
// 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期
resetExpire();
return true;
}
return false;
}
@Override
public void unlock() {
String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
// nil = false 1 = true 0 = false
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if (null == flag) {
throw new RuntimeException("this lock doesn't exists0");
}
}
private void resetExpire() {
String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
resetExpire();
}
}
}, (this.expireTime * 1000) / 3);
}
}
如果我们的分布式锁的服务器挂了,也就是单点故障,我们添加一个从机,但是复制是异步的。
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
让多个进程看到同一个锁监视器,而且要实现互斥,也是就只有一个线程能拿到线程锁!
X | MySQL | Redis | Zookeeper |
---|---|---|---|
互斥 | 利用mysq | 本身的互斥锁机制 | 利用setnx这样的互斥命令 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
实现分布式锁时需要实现的两个基本方法:
获取锁:
# 添加锁,NX是互斥,EX是设置超时时间
SET lock thread NX EX 10
释放锁:
# 释放锁,删除即可
DEL key
需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。
public interface ILock {
/**
* 非阻塞方式,尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
简单实现redis分布式锁:
public class SimpleRedisLock implements ILock {
// 业务名称
private String name;
private StringRedisTemplate stringRedisTemplate;
// 通过构造方法将name和stringRedisTemplate传入
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
long threadId = Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
改造上一章中加synchronized锁的代码:
synchronized (userId.toString().intern()) {
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
// 使用Redis分布式锁
// 创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁对象
boolean isLock = lock.tryLock(5);
// 加锁失败
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
// 释放锁
lock.unlock();
}
在获取锁时存入线程标识(可以用UUID表示)
在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
public class SimpleRedisLock implements ILock {
// 业务名称
private String name;
private StringRedisTemplate stringRedisTemplate;
// 通过构造方法将name和stringRedisTemplate传入
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标识是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
这里重点介绍Redis提供的调用函数,语法如下:
# 执行Redis命令
redis.call('命令名称', 'key', '其他参数', ...)
例如,我们要执行set name jack,则脚本是这样:
# 执行 set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name Rose,再执行get name,则脚本如下:
# 先执行 set name jack
redis.call('set', 'name', 'jack')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
127.0.0.1:6379> help @scripting
EVAL script numkeys key [key ...] arg [arg ...]
summary: Execute a Lua script server side
since: 2.6.0
例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
释放锁的业务流程是这样的:
如果用Lua脚本来表示则是这样的:
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标识
-- 获取锁中的标识,判断是否与当前线程标识一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
需求:基于Lua脚本实现分布式锁的释放锁逻辑
提示:RedisTemplate调用Lua脚本的API如下:
public class SimpleRedisLock implements ILock {
// 业务名称
private String name;
private StringRedisTemplate stringRedisTemplate;
// 通过构造方法将name和stringRedisTemplate传入
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
// 加载Lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlockL() {
// 调用Lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
基于Redis的分布式锁实现思路:
特性: