synchronize本地锁和reids锁

本地锁

有点比较快
缺点, 分布式情况下, 只能锁住当前节点服务

分布式锁

相比本地锁比较重量级
优点: 可以锁所有节点的服务
方式1: redis setnx实现分布式锁, 需要保证加锁的原子性, 解锁的原子性以及业务超时锁的自动续期三个问题
方式2: redission实现分布式锁

问题 使用redis的setnx实现分布式锁的问题

设置超时时间, 但是还有问题, 如果在设置超时时间时候异常了呢? 保证setnx的原子性, 设置值时候, 就设置了过期时间.
但是比如设置了10秒, 业务执行了30秒了, 又会出问题?

  1. 业务超时, 执行完之后删的锁其实是其他线程B进来后B的锁.

值设置为一个uuid, 删除锁的时候拿到这个值, 看是不是和自己设置时候的uuid相同, 相同才删除.

  1. 获取这个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);
  1. 还有一个问题, 业务没执行完呢, 设置的锁超时了, 要自动续期
  • 简单处理的话, 将超时时间设置足够长, 不过这虽然简单, 但是不是一个最佳解决方案
  • 自动续期

redission

redis distributed lock
参考文档
使用redisson

  1. 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.15.5</version>
</dependency>

  1. 配置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自动续期

  1. 锁的自动续期, 如果业务超长, 会自动续期, 不用担心业务时间长, 锁自动过期被删掉的问题. 默认加的时间是30秒.
  2. 加锁的业务完成, 就不会再给当前锁续期了, 即使不手动删除锁, 也会在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秒.

缓存一致性问题

数据库中的数据和缓存中如何保持一致?
数据库中数据修改了, 数据和缓存中就不一致了, 有两种方案

  1. 双写模式: 写数据库, 同时写入缓存
  2. 失效模式: 写入数据库, 删掉缓存, 下次读取时候从数据库中获取放入缓存.

写模式:
读写加锁: 适用于读多写少的数据, 否则大量加锁, 反而会很慢
引入canal+binlog, 感知数据库的更新去更新
读多写多的直接去数据库查询就行

双写模式,失效模式, 分布式多线程下都是不安全. 可加锁

springCache

很多个方法都要用到分布式缓存, 都这么去写比较麻烦, 可以用springcache整合, 简化操作.

  1. 引入依赖
<!-- spring cache -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  1. 自动导入的缓存
    CacheAutoConfiguration会自动导入RedisCacheConfiguration, 自动配置好了RedisCacheManager.

  2. 我们要干什么?
    配置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:在类级别共享一些常见的缓存相关设置。

  1. 开启缓存功能
...
@EnableCaching
public class CouplingProductApplication {
...
}
  1. 只需要使用注解完成缓存操作
// 缓存放到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;
}
  1. 配置, 比如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;
  }
}
  1. 修改数据时清空缓存
...
// 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值