2. 分散ロック
1. 分散ロックの原理
分散ロックとローカル ロックの本質は実際には同じであり、両方とも並列操作を直列操作に変換します。
2. 分散ロックの一般的なソリューション
2.1 データベース
可以利用MySQL隔离性:唯一索引
use test;
CREATE TABLE `DistributedLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性
lock(name,desc){
insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}
unlock(name){
delete from DistributedLock where name = #{name}
}
排他的ロックを使用して、更新用の select … where … を実装できます。
楽観的ロック: データにデータ セキュリティの問題はないと楽観的に信じ、問題がある場合は再試行します。
select ...,version;
update table set version+1 where version = xxx
2.2 リディス
setNX: setNX(key,value): キーが存在しない場合は、キーの値を追加します。それ以外の場合、追加は失敗します。Redisson
2.3動物園の飼育員
3.Redis は分散ロックを実装します
Redis では、setNX コマンドを使用してロックのプリエンプションが実現されます。このコマンドを使用して分散ロックを実装するための基本コードは次のとおりです。
public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
String keys = "catalogJSON";
// 加锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111");
if(lock){
// 加锁成功
Map<String, List<Catalog2VO>> data = getDataForDB(keys);
// 从数据库中获取数据成功后,我们应该要释放锁
stringRedisTemplate.delete("lock");
return data;
}else{
// 加锁失败
// 休眠+重试
// Thread.sleep(1000);
return getCatelog2JSONDbWithRedisLock();
}
}
上記のコードには実際にはいくつかの問題があります。まず、getDataForDB(keys) メソッドで例外が発生した場合、キーは削除されず、ロックも解放されないため、デッドロックが発生します。この問題に対処するには、次のようにします。これは、有効期限を設定することで解決されます。具体的なコードは次のとおりです。
public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
String keys = "catalogJSON";
// 加锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111");
if(lock){
// 给对应的key设置过期时间
stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
// 加锁成功
Map<String, List<Catalog2VO>> data = getDataForDB(keys);
// 从数据库中获取数据成功后,我们应该要释放锁
stringRedisTemplate.delete("lock");
return data;
}else{
// 加锁失败
// 休眠+重试
// Thread.sleep(1000);
return getCatelog2JSONDbWithRedisLock();
}
}
上記の方法で getDataForDB メソッドの例外の問題は解決しましたが、expired メソッドが実行される前に例外が発生した場合はどうなるでしょうか。これにより、先ほど紹介したデッドロックの問題も発生します。現時点では、setNx の操作と有効期限の設定によってアトミック性が確保できることを期待しています。
現時点では、setIfAbsent メソッドで有効期限を指定して、このアトミックな動作を保証することもできます。
public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
String keys = "catalogJSON";
// 加锁 在执行插入操作的同时设置了过期时间
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",30,TimeUnit.SECONDS);
if(lock){
// 给对应的key设置过期时间
stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
// 加锁成功
Map<String, List<Catalog2VO>> data = getDataForDB(keys);
// 从数据库中获取数据成功后,我们应该要释放锁
stringRedisTemplate.delete("lock");
return data;
}else{
// 加锁失败
// 休眠+重试
// Thread.sleep(1000);
return getCatelog2JSONDbWithRedisLock();
}
}
ロックを取得するためのビジネスの実行時間が比較的長く、設定した有効期限を超える場合、ビジネスが完了する前にロックが解放され、その後別のリクエストが入ってきてキーが作成される可能性があります。処理が完了した後、キーを削除すると、他人のキーを削除することができますが、この場合はどうすればよいでしょうか? この場合、照会できるロック情報は UUID によって区別されます。具体的なコードは次のとおりです。
public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
String keys = "catalogJSON";
// 加锁 在执行插入操作的同时设置了过期时间
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
if(lock){
// 给对应的key设置过期时间
stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
// 加锁成功
Map<String, List<Catalog2VO>> data = getDataForDB(keys);
// 获取当前key对应的值
String val = stringRedisTemplate.opsForValue().get("lock");
if(uuid.equals(val)){
// 说明这把锁是自己的
// 从数据库中获取数据成功后,我们应该要释放锁
stringRedisTemplate.delete("lock");
}
return data;
}else{
// 加锁失败
// 休眠+重试
// Thread.sleep(1000);
return getCatelog2JSONDbWithRedisLock();
}
}
上記のキーの値のクエリとキーの削除は、実際にはアトミックな操作ではありません。これにより、キーのクエリ後に時間が経過し、キーが削除されます。その後、他のリクエストによって新しいキーが作成され、その後、元の実行が実行されます。このキーを削除した後、他の人のキーが削除されるという別の状況が発生します。現時点では、クエリと削除がアトミックな動作であることを確認する必要があります。
public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
String keys = "catalogJSON";
// 加锁 在执行插入操作的同时设置了过期时间
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
Map<String, List<Catalog2VO>> data = null;
try {
// 加锁成功
data = getDataForDB(keys);
}finally {
String srcipts = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end ";
// 通过Redis的lua脚本实现 查询和删除操作的原子性
stringRedisTemplate.execute(new DefaultRedisScript<Long>(srcipts,Long.class)
,Arrays.asList("lock"),uuid);
}
return data;
}else{
// 加锁失败
// 休眠+重试
// Thread.sleep(1000);
return getCatelog2JSONDbWithRedisLock();
}
}
https://space.bilibili.com/435498550 分散ロックの実装
4.レディソン分散ロック
4.1 レディソンの統合
対応する依存関係を追加する
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.1</version>
</dependency>
対応する構成クラスを追加します
@Configuration
public class MyRedisConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
// 配置连接的信息
config.useSingleServer()
.setAddress("redis://192.168.56.100:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4.2 リエントラントロック
/**
* 1.锁会自动续期,如果业务时间超长,运行期间Redisson会自动给锁重新添加30s,不用担心业务时间,锁自动过期而造成的数据安全问题
* 2.加锁的业务只要执行完成, 那么就不会给当前的锁续期,即使我们不去主动的释放锁,锁在默认30s之后也会自动的删除
* @return
*/
@ResponseBody
@GetMapping("/hello")
public String hello(){
RLock myLock = redissonClient.getLock("myLock");
// 加锁
myLock.lock();
try {
System.out.println("加锁成功...业务处理....." + Thread.currentThread().getName());
Thread.sleep(30000);
}catch (Exception e){
}finally {
System.out.println("释放锁成功..." + Thread.currentThread().getName());
// 释放锁
myLock.unlock();
}
return "hello";
}
4.3 読み取り/書き込みロック
業務内容に応じて、読み込みと書き込みに分けることができますが、読み込みは実際にはデータに影響を与えません、読み込みもシリアル処理すると効率が非常に悪くなりますので、この時点で解決できます。読み取り/書き込みロックによる問題。
@GetMapping("/writer")
@ResponseBody
public String writerValue(){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
// 加写锁
RLock rLock = readWriteLock.writeLock();
String s = null;
rLock.lock(); // 加写锁
try {
s = UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set("msg",s);
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping("/reader")
@ResponseBody
public String readValue(){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
// 加读锁
RLock rLock = readWriteLock.readLock();
rLock.lock();
String s = null;
try {
s = stringRedisTemplate.opsForValue().get("msg");
}finally {
rLock.unlock();
}
return s;
}
読み書きロックは、読み出しと読み出しの動作のみが相互に影響を及ぼさない共有ロックであり、書き込み動作がある限り相互排他ロック(排他ロック)となります。
4.4 ロックアウト
Redisson に基づくRedisson 分散ロック ( CountDownLatch ) Java オブジェクトは、同様のインターフェイスと使用法RCountDownLatch
を採用しています。java.util.concurrent.CountDownLatch
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor(){
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.trySetCount(5);
try {
door.await(); // 等待数量降低到0
} catch (InterruptedException e) {
e.printStackTrace();
}
return "关门熄灯...";
}
@GetMapping("/goHome/{id}")
@ResponseBody
public String goHome(@PathVariable Long id){
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown(); // 递减的操作
return id + "下班走人";
}
4.5 セマフォ
Redisson の Redis に基づく分散セマフォ ( Semaphore ) Java オブジェクト
RSemaphore
java.util.concurrent.Semaphore
同様のインターフェイスと使用法を採用しています。同時に、非同期 (Async)、反射型 (Reactive)、およびRxJava2 標準インターフェイスも提供します。
@GetMapping("/park")
@ResponseBody
public String park(){
RSemaphore park = redissonClient.getSemaphore("park");
boolean b = true;
try {
// park.acquire(); // 获取信号 阻塞到获取成功
b = park.tryAcquire();// 返回获取成功还是失败
} catch (Exception e) {
e.printStackTrace();
}
return "停车是否成功:" + b;
}
@GetMapping("/release")
@ResponseBody
public String release(){
RSemaphore park = redissonClient.getSemaphore("park");
park.release();
return "释放了一个车位";
}
4.6 キャッシュデータの一貫性の問題
上記の 2 つの解決策からどのように選択すればよいでしょうか?
- すべてのキャッシュされたデータに有効期限を追加し、データの有効期限が切れた後に更新操作をアクティブにトリガーします。
- 読み取り/書き込みロックを使用して処理します。読み取り操作と読み取り操作は相互に影響しません。
二重書き込みモードであっても失敗モードであっても、キャッシュの不整合の問題が発生します。つまり、複数のインスタンスが同時に更新されると、何かが起こります。何をするか?
- ユーザー緯度データ (注文データ、ユーザー データ) の場合、同時実行の可能性は非常に小さいです。この問題を考慮する必要はありません。キャッシュされたデータと有効期限により、時々読み取りのアクティブな更新がトリガーされる可能性があります
。 。 - メニューや商品紹介などの基本データであれば、canalを利用してbinlogを購読することもできます。
- データのキャッシュ + 有効期限も、キャッシュに関するほとんどのビジネス要件を解決するのに十分です。
- ロックは読み取りと書き込みの同時実行を保証するために使用され、キューは書き込みと書き込み時に順番に配置されます。読んでも構いません。したがって、読み取り/書き込みロックを使用するのが適しています。(企業は
ダーティ データを気にせず、一時的なダーティ データは無視されます)
要約:
- キャッシュに入れることができるデータには、極端に高いリアルタイム性と一貫性の要件があってはなりません。したがって、データをキャッシュするときは、有効期限を追加して、毎日最新のデータを確実に取得できるようにします。
- システムを過度に設計したり、複雑さを増大させたりすべきではありません
- リアルタイム性と一貫性の要件が高いデータに遭遇した場合は、たとえ速度が遅くてもデータベースをチェックする必要があります。
3. スプリングキャッシュ
依存
<!-- 导入SpringCache的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
構成クラス
- カスタムのシリアル化メソッドを指定します。Redis クライアント上の値の形式は、より便利な json 形式です。
- 有効期限を設定するには、yaml 構成ファイルで spring.cache.redis.time-to-live を構成して有効期限を指定するだけでなく、構成ファイル内の対応するメソッドを呼び出して、構成された有効期限を取得する必要もあります。 。
- 侵入を防ぐように設定します。つまり、多数の null 値クエリが redis をバイパスしないように null 値をキャッシュします。cache-null-values: true # キャッシュの侵入を防ぐために null 値をキャッシュするかどうかも設定する必要があります。対応するメソッドを呼び出して実装するための構成ファイル。
- キープレフィックスを設定し、プレフィックスを指定します key-prefix: pro_; use-key-prefix: true (デフォルトは true) 実装するには、構成クラス内の対応するメソッドを呼び出す必要もあります
スタートアップクラス
- キャッシュを有効にするためにスタートアップ クラスにアノテーション **@EnableCaching** を追加します
@Configuration
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 指定自定义的序列化的方式
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
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;
}
}
アプリケーション.yml
spring:
redis:
host: 192.168.56.100
port: 6379
cache:
type: redis # SpringCache 缓存的类型是 Redis
redis:
time-to-live: 60000 # 指定缓存key的过期时间
# key-prefix: bobo_
use-key-prefix: true
cache-null-values: true # 是否缓存空值,防止缓存穿透
使用例
/**
* 查询出所有的商品大类(一级分类)
* 在注解中我们可以指定对应的缓存的名称,起到一个分区的作用,一般按照业务来区分
* @Cacheable({"catagory","product"}) 代表当前的方法的返回结果是需要缓存的,
* 调用该方法的时候,如果缓存中有数据,那么该方法就不会执行,
* 如果缓存中没有数据,那么就执行该方法并且把查询的结果缓存起来
* 缓存处理
* 1.存储在Redis中的缓存数据的Key是默认生成的:缓存名称::SimpleKey[]
* 2.默认缓存的数据的过期时间是-1永久
* 3.缓存的数据,默认使用的是jdk的序列化机制
* 改进:
* 1.生成的缓存数据我们需要指定自定义的key: key属性来指定,可以直接字符串定义也可以通过SPEL表达式处理:#root.method.name
* 2.指定缓存数据的存活时间: spring.cache.redis.time-to-live 指定过期时间
* 3.把缓存的数据保存为JSON数据
* SpringCache的原理
* CacheAutoConfiguration--》根据指定的spring.cache.type=reids会导入 RedisCacheAutoConfiguration
* @return
*/
@Trace
@Cacheable(value = {
"catagory"},key = "#root.method.name",sync = true)
@Override
public List<CategoryEntity> getLeve1Category() {
System.out.println("查询了数据库操作....");
long start = System.currentTimeMillis();
List<CategoryEntity> list = baseMapper.queryLeve1Category();
System.out.println("查询消耗的时间:" + (System.currentTimeMillis() - start));
return list;
}
SpringCache の欠点:
1).読み取りモード
- キャッシュの侵入: null データをクエリします。解決できます。cache-null-values=true
- キャッシュの内訳: 大量の同時クエリが受信され、期限切れ直前のデータをクエリします。解決策: 分散ロック同期 = 真のローカル ロック
- キャッシュなだれ: 多数のキーが同時に期限切れになります。解決策: 有効期限 time-to-live=60000 を追加して有効期限を指定します
2).書き込みモード
- 読み書きロック
- Canal を導入し、binlog ログ ファイルを監視してデータを同期的に更新します
- データベースに直接アクセスしてデータを読み取るだけで、さらに読み、さらに書き込むことができます。
要約:
- 通常のデータ (読み取りを多くし、書き込みを少なくする): 適時性とデータの一貫性の要件が高くない場合は、SpringCache を完全に使用できます。
- 特別な状況: 特別な状況は特別に処理されます。