在有限的资源情况下或某个场景下,需要控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。
Java中实现锁:synchronized 关键字、并发包的类
这种锁的方式是在代码中的某个方法或者某个类中限定了,这导致这样的锁只对当前单个 JVM 有效。A服务器上的锁和B服务器上的锁是独立的,互不影响的。
锁的获取可以粗暴的理解为,在要执行某段代码前定义: 必须获取到某个信物或者武林盟主的令牌,获取到了这个信物的才能干某件事,没有获取到的就不能执行。
并同时可以定义在获取到信物后的一些使用规则: 可以指定这个信物它的 自开始使用的有效时间、过期了自动续期、信物到期了是否要回收/销毁 等操作。
为啥需要分布式锁?
在有限的资源情况下或某个场景下,需要控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。
比如说,代码中的定时任务,由于代码部署了多台服务器,但设定的定时任务只需要在多台服务器上有一台服务器执行了就行。
单机锁只对单个JVM有效。
怎么保证同一时间只有 1 个服务器能抢到锁?
核心思想:先来操作的人先把数据改成自己的标识(比如服务器的ip),后来的人发现标识已存在,表明抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人(线程)继续抢锁。
通过数据库(如MySQL)中实现设置抢锁机制:
Redis 实现:内存数据库,读写速度快。支持 setnx、lua脚本,比较方便实现分布式锁。比较主流的实现方式。
setnx脚本命令语法:set if not exists 如果不存在,则进行设置;只有设置成功才会返回true,否则返回 false。
set name ming nx
抢到锁,执行完相关代码后,要记得释放锁。(有的时候执行方法的时间会比设置的锁的过期时间短,这时候可以提前释放锁,腾地方,及时给别的人用)
Redis在使用锁时,一定要设置锁的过期时间,到期直接释放。即使抢到锁的服务器挂掉了,也能正常过期释放。
如果获得锁的方法执行的时间过长,但是锁设置的过期时间到了 / 提前过期了?
会产生一系列的连锁反应的问题:释放掉别人的锁,这样还是会存在多个方法同时进行的情况?
在方法A中释放锁的时候,有可能在判断过程中出现问题;比如上一步刚执行判断出是自己的锁,但这时候锁设置的过期时间刚好到期了,这时候别的B方法又获取到锁,执行B的方法,这时候A再去释放锁这时候还是释放了别人的锁。
解决方案: 利用 Redis 的原子性操作。通过 Redis + Lua 脚本实现,类似Java里面的事务处理机制。
Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办?
解决方案:https://blog.csdn.net/feiying0canglang/article/details/113258494 拒绝自己实现!!!直接用现成的。
Zookeeper 实现(不推荐)
Redisson: Java 客户端、数据网格、实现了很多 Java 里支持的接口和数据结构。
Redisson 是一个 Java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用Java代码里的集合一样使用 Redis,完全感知不到 Redis 的存在(或者说使用过程中感知不到是在操作一个额外的东西)。
Redisson 2 种引入方式:
Spring boot start 引入(引入很方便,但是不推荐,和Springboot之间有版本对应关系,redisson版本迭代太快,容易冲突): https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
直接引入: https://github.com/redisson/redisson#quick-start 官方使用教程,Quick start
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.34.1</version>
</dependency>
Java 新建 Redisson配置类
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
/**
* 定义redis连接地址
*/
private String host;
/**
*定义redis连接端口号
*/
private String port;
@Bean
public RedissonClient redissonClient() {
// 1. 创建配置
Config config = new Config();
/**
* String redissonAddress = "redis:127.0.0.1:6379";
* 这里可以不写死,用在yml配置文件的配置,动态读取redis配置
* @ConfigurationProperties(prefix = "spring.redis") prefix 添加读取的前缀
*/
String redissonAddress = String.format("redis://%s:%s", host, port);
config.useSingleServer()
.setAddress(redissonAddress)
// 分布式锁的存储可以和其他的缓存数据隔开,选择其他的库
.setDatabase(1);
// 2. 创建实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
注意: redissonClient 操作 redis 的语法中,设置redis的key是通过 “getXxx()”来实现的
比如,redisson在redis中创建一个list:RList<String> rList = redissonClient.getList("test-list");
这个“test-list”就是对应redis中设置的集合的key,rList.add("mmm");
值就是通过add方法。
测试类,通过 Redisson 实现操作 redis存取数据:
@SpringBootTest
public class RedissonTest {
@Resource
private RedissonClient redissonClient;
@Test
void test() {
// list jvm
List<String> list = new ArrayList<>();
list.add("ming");
System.out.println(list.get(0));
list.remove(0);
// redis
RList<String> rList = redissonClient.getList("test-list");
rList.add("mmm");
System.out.println(rList.get(0));
// rList.remove(0);
// map
Map<String, Integer> map = new HashMap<>();
map.put("ming", 12);
map.get("ming");
RMap<Object, Object> rMap = redissonClient.getMap("test-map");
rMap.put("ming", 12);
// set
}
}
比如定时任务,给定时任务加锁:
定时任务中只有获取到了某个锁,才能继续正式执行定时任务中的代码,进行定时任务的业务逻辑。
@Component
@Slf4j
public class PreCacheTask {
@Resource
private RedisTemplate redisTemplate;
@Resource
private RedissonClient redissonClient;
/**
* 每天执行一次 预热加载公共用户信息
*/
@Scheduled(cron = "30 17 17 * * *")
public void doCacheCommonUserData() {
// 通过redissonClient 添加并设置锁的 key
RLock lock = redissonClient.getLock("user-center:PreCacheTask:doCacheCommonUserData:lock");
try {
// 判断锁,只有当获取到了锁才执行这个定时任务 只有一个线程能获取到锁
if (lock.tryLock(0, 30000L, TimeUnit.MILLISECONDS)) {
oneScheduleTaskCode();
}
} catch (InterruptedException e) {
log.error("doCacheCommonUserData error", e);
throw new RuntimeException(e);
} finally {
// 加判断 lock.isHeldByCurrentThread() 实现只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
// 释放锁 释放资源一类的操作一定要写在finall里
lock.unlock();
}
}
}
/**
* 某个定时任务要执行的业务代码逻辑
*/
private void oneScheduleTaskCode() {
ValueOperations valueOperations = redisTemplate.opsForValue();
Map<String, Object> mapKey = new HashMap<>();
mapKey.put("mingString", "dong");
mapKey.put("mingInt", 1);
mapKey.put("mingDouble", "2.1");
User user = new User();
user.setId(1L);
user.setUserName("mingdongdong");
mapKey.put("mingUser", user);
mapKey.forEach((key, value) -> {
String redisKey = String.format("user-center:%s", key);
// 将数据存到 redis 缓存,并设置过期时间
valueOperations.set(redisKey, value, 10, TimeUnit.MINUTES);
});
// 查
String mingString = (String) valueOperations.get("user-center:mingString");
int mingInt = (Integer) valueOperations.get("user-center:mingInt");
Object mingDouble = valueOperations.get("user-center:mingDouble");
Object mingUser = valueOperations.get("user-center:mingUser");
System.out.println(mingString);
System.out.println(mingInt);
System.out.println(mingDouble);
System.out.println(mingUser);
RedisOperations operations = valueOperations.getOperations();
RedisSerializer keySerializer = operations.getKeySerializer();
System.out.println(keySerializer);
}
}
lock.tryLock(0, 30000L, TimeUnit.MILLISECONDS)
tryLock方法,这里的tryLock(waitTime, -1, unit)有三个参数:
waiTime
参数设置为 0,只抢一次,抢不到就放弃。锁的理解:
锁的获取可以粗暴的理解为,在要执行某段代码前定义: 必须获取到某个信物或者武林盟主的令牌,获取到了这个信物的才能干某件事,没有获取到的就不能执行。
并同时可以定义在获取到信物后的一使用规则: 可以指定这个信物它的 自开始使用的有效时间、过期了自动续期、信物到期了是否要回收/销毁 等操作。
redisson 中提供的续期机制,给redis自动续期
开启一个监听线程,如果加锁的方法还没执行完,就帮你重置 redis 锁的过期时间。
原理:
监听当前加锁执行方法的线程,每 10 秒执行续期一次。
只要方法没有执行完,代码中没有执行释放锁操作,就会保持这个 10 秒自动续期。
理论上 ,加锁的方法中没有释放锁的代码,Watch dog 会一直续期锁的过期时间。
所以,使用锁,一定要在最后有释放锁的操作。
如果线程挂掉(注意debug模式也会被它当成服务器宕机),则不会自动续期了
面试题:看门狗机制下,能自动续期,为什么还要设置30s的自动续期?
防止宕机,redis设置过期时间是一个必备的保险操作。
https://blog.csdn.net/qq_26222859/article/details/79645203