分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
-
性能:redis最高
-
可靠性:zookeeper最高
但是畅购项目并没有使用zookeeper进行开发,功能由Eureka接替,我也没有在项目实现改造。下一篇文章再说利用模板设计模式实现分布式锁的问题。
借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
-
多个客户端同时获取锁(setnx)
-
获取成功,执行业务逻辑,执行完成释放锁(del)
-
其他客户端等待重试
基本思想就是这样改造,为了解决 缓存击穿 击穿是一个热点key失效,执行redis的setnx命令
uuid是保证给自己的锁唯一标志,防止误删。
同时string script是lua脚本的执行命令,获取值和对比是原子操作。
核心在于加锁和解锁都是原子性的,加锁同时设置过期时间,保证数据一致性。String uuid = UUID.randomUUID().toString(); Boolean aBoolean = this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,10,TimeUnit.SECONDS); Content content=new Content(); //判断是否拿到锁 if(aBoolean){ String jsonContent = redisTemplate.boundValueOps("content_" + id).get(); if(jsonContent!=null){ List<Content> contents; contents = JSON.parseObject(jsonContent, List.class); return contents; } content.setCategoryId(id); content.setStatus("1"); // 2. 释放锁 del String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; this.redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), Arrays.asList(uuid)); }else { //其他请求尝试获取锁 findByCategory(id); } return contentMapper.select(content);
备注:其实最好的方式还是redission加上AOP环绕通知,做成注解实现,具体查阅google
另外就是我们可以利用springCache进行改造,简化缓存开发。
第一步就是引入依赖
第二部自然就是配置
由于我们的中间件redis作为我们的缓存所以就要引RedisCacheConfiguration配置类对象,并且绑定我们的配置
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
/**
* 配置文件没有用上ttl
* 1 原来配置文件的配置类是这样
* @ConfigurationProperties(
* prefix = "spring.cache"
* 2 让他生效 开启属性绑定生效
* @return
*/
RedisCacheConfiguration getRedisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
//将配置文件所有配置生效
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
这是properties的配置,缓存存活时间,前缀,是否使用空值做缓存
第三就是几个注解的使用
@Cacheable 触发将数据保存到缓存
@CacheEvict 触发将数据从缓存删除
@CachePut 不影响方法执行更新缓存
@Cacheing 组合以上操作
@CacheConfig 在类级别共享缓存的相同配置
第一个注解value就是你的缓存分区,key就是(key,value)的key.
可以采用spel表达式,就是第一个注解,key值就是你的方法名字。
sync属性就能保证加锁,顺序去缓存,解决缓存问题。
@CacheEvict 清空缓存
因为我们更新数据之后,就要清除缓存保证数据一致性
这是清空该分区缓存的key
@CacheEvict(value = {“category”}, key = “‘Level1Categorys’”)
清空该分区的所有缓存
@CacheEvict(value = {“category”},allEntries = true)
清除多个缓存操作,使用它的evict的属性
@Caching(evict =
{@CacheEvict(value = {“category”}, key = “‘Level1Categorys’”),
@CacheEvict(value = {“category”}, key = “‘getCatelogJson’”)
})