锁
本地锁
有点比较快
缺点, 分布式情况下, 只能锁住当前节点服务
分布式锁
相比本地锁比较重量级
优点: 可以锁所有节点的服务
方式1: redis setnx实现分布式锁, 需要保证加锁的原子性, 解锁的原子性以及业务超时锁的自动续期三个问题
方式2: redission实现分布式锁
问题 使用redis的setnx实现分布式锁的问题
设置超时时间, 但是还有问题, 如果在设置超时时间时候异常了呢? 保证setnx的原子性, 设置值时候, 就设置了过期时间.
但是比如设置了10秒, 业务执行了30秒了, 又会出问题?
- 业务超时, 执行完之后删的锁其实是其他线程B进来后B的锁.
值设置为一个uuid, 删除锁的时候拿到这个值, 看是不是和自己设置时候的uuid相同, 相同才删除.
- 获取这个uuid时, 远程耗时比较长, 获取到了自己的uuid, 但是获取后, key已经失效, 变成别人的了, 又会出现问题.
使用Lua脚本解锁, 保证解锁的原子性
String uuid = UUID.randomUUID().toString();
// Lua 脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
- 还有一个问题, 业务没执行完呢, 设置的锁超时了, 要自动续期
- 简单处理的话, 将超时时间设置足够长, 不过这虽然简单, 但是不是一个最佳解决方案
- 自动续期
redission
redis distributed lock
参考文档
使用redisson
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.5</version>
</dependency>
- 配置bean
@Configuration
public class MyRedissonConfiguration {
/**
* 所有对redisson的操作, 都通过这个对象
* @return
* @throws IOException
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.242.128:6379");
return Redisson.create(config);
}
}
redisson自动续期
- 锁的自动续期, 如果业务超长, 会自动续期, 不用担心业务时间长, 锁自动过期被删掉的问题. 默认加的时间是30秒.
- 加锁的业务完成, 就不会再给当前锁续期了, 即使不手动删除锁, 也会在30秒后自动删除, 所以不会有死锁问题.
可重入锁 Reentrant Lock
public void test(){
RLock lock = redissonClient.getLock("my-lock");
lock.lock();
try {
System.out.println("加锁成功, 执行业务..." + Thread.currentThread().getId());
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放锁....."+Thread.currentThread().getId());
lock.unlock();
}
}
如果用lock.lock(10, TimeUnit.SECONDS)这种设置超时时间, 就不会自动续期了, 业务执行比10秒长的话, 就会出问题. 如果没指定时间, 就是默认30秒, 看门狗自动续期.
tryLock方法, 可指定最多等待多长时间的获取锁的时间, 如果指定时间还没获到锁就不等了.
最佳实战
还是推荐自定义时间, 设置为30秒.
缓存一致性问题
数据库中的数据和缓存中如何保持一致?
数据库中数据修改了, 数据和缓存中就不一致了, 有两种方案
- 双写模式: 写数据库, 同时写入缓存
- 失效模式: 写入数据库, 删掉缓存, 下次读取时候从数据库中获取放入缓存.
写模式:
读写加锁: 适用于读多写少的数据, 否则大量加锁, 反而会很慢
引入canal+binlog, 感知数据库的更新去更新
读多写多的直接去数据库查询就行
双写模式,失效模式, 分布式多线程下都是不安全. 可加锁
springCache
很多个方法都要用到分布式缓存, 都这么去写比较麻烦, 可以用springcache整合, 简化操作.
- 引入依赖
<!-- spring cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
-
自动导入的缓存
CacheAutoConfiguration会自动导入RedisCacheConfiguration, 自动配置好了RedisCacheManager. -
我们要干什么?
配置yaml
spring:
...
cache:
type: redis
redis:
# 缓存时间, 如果不在配置文件中配置CacheProperties的话, 这里也不会生效
time-to-live: 3600000
# 缓存的key前缀
key-prefix: cache_
# 开启key前缀
use-key-prefix: true
# 是否缓存空值, 缓存空值可以防止缓存穿透
cache-null-values: true
使用缓存
-
@Cacheable: 触发将数据保存到缓存。
-
@CacheEvict: 触发将数据从缓存删除, 失效模式, 修改后清空缓存。
-
@CachePut: 在不干扰方法执行的情况下更新缓存, 双写模式, 将更新的数据写入缓存, 双写模式必须要有返回值。
-
@Caching:重新组合上面的多个缓存操作。
-
@CacheConfig:在类级别共享一些常见的缓存相关设置。
- 开启缓存功能
...
@EnableCaching
public class CouplingProductApplication {
...
}
- 只需要使用注解完成缓存操作
// 缓存放到category中, 如果缓存中有数据, 就会直接从category缓存中取数据, 不会执行方法了, 如果没有, 就会执行这个方法, 将返回的结果放入category缓存中.
@Cacheable(value = {"category"}, key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categories() {
List<CategoryEntity> categories = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
return categories;
}
- 配置, 比如value用json序列化
@Configuration
// 不然配置无法生效
@EnableConfigurationProperties(CacheProperties.class)
// 开启缓存
@EnableCaching
public class MyCacheConfiguration {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// string序列化key
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 值用json序列化, 默认是byte
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()))
;
Redis redis = cacheProperties.getRedis();
// 下面都是从RedisCacheConfiguration拿来的
if (redis.getTimeToLive() != null) {
redisCacheConfiguration = redisCacheConfiguration.entryTtl(redis.getTimeToLive());
}
if (redis.getKeyPrefix() != null) {
redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redis.getKeyPrefix());
}
if (!redis.isCacheNullValues()) {
redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
}
if (!redis.isUseKeyPrefix()) {
redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
}
return redisCacheConfiguration;
}
}
- 修改数据时清空缓存
...
// key的值是查询放入缓存对应的方法签名, 因为用的是el表达式, 所以key的值要加上单引号
// 删除单个
//@CacheEvict(value = {"category"}, key = "'getLevel1Categories'")
// 删除多个第一种方式
//@Caching(evict = {@CacheEvict(value = {"category"}, key = "'getLevel1Categories'"), @CacheEvict(value = {"category"}, key = "'xxx2'")})
// 删除多个第二种方式
@CacheEvict(value = {"category"}, allEntries=true)
public void updateCascade(CategoryEntity category) {
...
}
spring cache缺点
只有@Cacheable注解中有个sync = true的时候才只会在查询时候加个本地同步锁synchronize.
所以, 读模式spring cache加上sync = true, 可以防止缓存击穿问题, 虽然是本地锁, 但是如果该服务有10个节点, 最多也只会查10次数据库, 没什么影响, 不用加分布式锁也可以
但是对于写模式, spring cache是没有管的, 没有锁
所以总结下来: 常规数据(读多写少, 即时性,一致性要求不高的)完全可以使用spring cache.
特殊数据: 特殊处理, 比如使用canal