1つは、ローカルロックの問題を追加する
1.1ローカルロック分析
以前のRedisキャッシュでの導入に続いて、キャッシュの浸透(空の結果キャッシュ)とキャッシュアバランシェの問題(ランダムな値の追加)を簡単に解決できます。キャッシュの内訳の問題には、ロック方法を使用できますが、このロックはどのような種類ですかロックの使用が必要であり、このロックを追加する方法も非常に特殊です。不注意はさまざまな問題を引き起こす可能性があります。
ここでは、最初にローカルロックを追加する場合のいくつかの問題をテストします。
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//给缓存中放json字符串,拿出的json字符串,还要逆转为能用的对象类型;【序列化与反序列化】
/**
* 1.空结果缓存:解决缓存穿透
* 2.设置过期时间(加随机值): 解决缓存雪崩
* 3.加锁: 解决缓存击穿
*/
//1. 加入缓存逻辑,缓存中存的数据是json字符串
//JSON好处:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//2.缓存中没有,查询数据库
System.out.println("缓存不命中...查询数据库...");
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//每次调用这个方法,都有查询数据库,得到返回值,效率非常低下,我们需要加入缓存
//3. 查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return catalogJsonFromDb;
}
System.out.println("缓存命中...直接返回...");
//从缓存中获取的JSON,再转为我们指定的对象返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程,使用this,this代表的就是当前对象
//1.synchronized(this):SpringBoot所有的组件在容器中都是单例的
/**
* 100万个请求同时进来,进来以后就先锁住,接下来这100万个请求就来竞争锁,假设有一个竞争上来了,那他就去执行数据库查询,查询完以后返回,释放锁
* 别的请求再一进来,再去查数据库就是不合理的,相当于我们虽然锁住了,相当于再去排队查数据库。
* 所以拿到锁以后,进来要做的第一件事,就是再看一下缓存里面有没有,如果有了说明是上一个人执行完放好的,如果没有你才需要再去查
*/
//TODO 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
synchronized (this) {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
//缓存不为null直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
System.out.println("查询了数据库....");
//将数据库的多次查询变为一次
Map<String, List<Catelog2Vo>> parentCid = selectFromDB();
return parentCid;
}
}
この方法でロックするのは適切ですか?
単一のアプリケーションの場合、つまり、プロジェクトはTomatとサーバーにのみデプロイされるため、ロックしても問題ありません。ただし、配布すると変更されます
分散されている場合、私たちの一般的な状況は、負荷分散メカニズムのために、大きな同時実行性を想定して、多くのサーバーにサービスを配置することです。現在、サーバーはなく、すべてが10,000の同時実行性を受け入れます。これをロックして追加します。これは、現在のインスタンスを意味します。オブジェクト、これが同期コードブロックに追加したものであろうと、メソッド上のこれであろうと、これはロックとしての現在のインスタンスであり、現在のインスタンスはコンテナ内の単一のインスタンスですが、1つのプロジェクト、1つのコンテナ、つまり、1つの商品サービスに対して1つのコンテナーがあります。このように、1つの商品サービスに対して8つのマシンがあり、これは8つのマシンに相当します。8つのコンテナーは8つのインスタンスに相当します。したがって、これはそれぞれコンテナーのみを表します。現在のインスタンスのオブジェクト。つまり、これはそれぞれ異なるロックであり、最後に8つのロックを追加することに相当します。
結局、結果として生じる現象は、私たちの商品とサービスのこのロックは10,000リクエストをロックすることと同等であり、1つだけが入力されます。次に、サーバー2も10,000をロックすることと同等であり、1つだけが入力されます。結局、分散型の場合、それは数台のマシンに相当し、いくつかのスレッドをに入れます。これは、データベース内の同じデータをチェックするために、8つのスレッドが同時に入ってくることと同じです。
したがって、ローカルロックは現在のプロセスのみをロックできると述べました。大量の同時数百万のリクエストを実際に実装し、データベースのクエリに1つだけ残したい場合は、分散ロックを使用する必要があります。分散ロックの欠点は、パフォーマンスが比較的遅いことですが、現在のローカルロックはわずかに高速です。ただし、ローカルロックの欠点は、分散状況ではすべてのサービスをロックできないことです。
ただし、シナリオに基づいて、同期ロックを使用することもできます。製品サービスに100ユニットを配置しても、同時実行数は100万になります。データベースをチェックするために最大で100ユニットを配置します。では、どうしますか?データベースの圧力はそれほど大きくなく、ロックは設計ほど重くありません。
1.2ローカルロック圧力テスト
1.2.1問題の提示
1.最初にredisのキーを削除します
必要な効果は、コンソールに出力されると、キャッシュが失われ、データがチェックされ、データベースは1回しかチェックできないということです。複数回チェックされた場合、ロックは失敗します。
2.圧力テスト
3.テスト結果
4.結果分析
図に示すように、黒はロックされた部分を示します。スレッド1はロックを解除するために終了します。ロックが解除された後、結果はキャッシュに入れられていません。スレッド2はロックを取得して入りました。最初にキャッシュにデータがあるかどうかを確認する必要がありますが、スレッド1はデータをredisに入れます。これはネットワークの相互作用であり、非常に遅い可能性があります。その結果、データ2はそうではありません。キャッシュで見つかりました。データ、そしてデータベースもありません。したがって、これにより、データベースに2回クエリが実行されます。
1.2.2問題解決
この問題を解決するために、結果をキャッシュ操作に入れることができます。ロックを解除した後は行わないでください。データベースが見つかった場合は、結果がキャッシュに入れられないようにします。ロックされ、データベースに2回クエリを実行します。
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//给缓存中放json字符串,拿出的json字符串,还要逆转为能用的对象类型;【序列化与反序列化】
/**
* 1.空结果缓存:解决缓存穿透
* 2.设置过期时间(加随机值): 解决缓存雪崩
* 3.加锁: 解决缓存击穿
*/
//1. 加入缓存逻辑,缓存中存的数据是json字符串
//JSON好处:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//2.缓存中没有,查询数据库
System.out.println("缓存不命中...查询数据库...");
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//每次调用这个方法,都有查询数据库,得到返回值,效率非常低下,我们需要加入缓存
return catalogJsonFromDb;
}
System.out.println("缓存命中...直接返回...");
//从缓存中获取的JSON,再转为我们指定的对象返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程,使用this,this代表的就是当前对象
//1.synchronized(this):SpringBoot所有的组件在容器中都是单例的
/**
* 100万个请求同时进来,进来以后就先锁住,接下来这100万个请求就来竞争锁,假设有一个竞争上来了,那他就去执行数据库查询,查询完以后返回,释放锁
* 别的请求再一进来,再去查数据库就是不合理的,相当于我们虽然锁住了,相当于再去排队查数据库。
* 所以拿到锁以后,进来要做的第一件事,就是再看一下缓存里面有没有,如果有了说明是上一个人执行完放好的,如果没有你才需要再去查
*/
//TODO 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
synchronized (this) {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
//缓存不为null直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
System.out.println("查询了数据库....");
//将数据库的多次查询变为一次
Map<String, List<Catelog2Vo>> parentCid = selectFromDB();
//3. 查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(parentCid);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return parentCid;
}
}
したがって、データをチェックし、チェック後にキャッシュを配置する必要があります。これはアトミック操作であり、同じロックで実行されます。そうしないと、ロック全体の解放のタイミングの問題が発生し、 2つのデータベースをチェックするように導きました。
1.3分散状況でのローカルロックの問題
1.3.1商品とサービスの2つのコピーを作成する
1.3.2Jmeter圧力テスト
リクエストはnginxからゲートウェイに転送され、ゲートウェイはいくつかの商品やサービスに負荷分散されます
データベースはサービスごとにクエリされます。前述のように、ローカルロックを使用する場合、すべてのマイクロサービスをロックすることはできません。ローカルロックは現在のサービスのみをロックできます。すべてのサービスをロックするには、分散ロックを追加する必要があります。