1. ホーム
  2. データベース
  3. レディス

Redisの高同期スパイクを防ぐために、ソースコードソリューションを売られすぎ

2022-01-22 04:01:05

1: ソリューションのアイデア

アクティビティをredisに書き込み、redis self-decreaseコマンドでインベントリを減算する。

2:redisの定数を追加する

commons/constant/RedisKeyConstant.java

seckill_vouchers("seckill_vouchers:","seckill_vouchers's key"),

3: redis設定クラスを追加する

4: ビジネスレイヤーの変更

さっそく、ソースコードを見てみましょう。

1: ビジネスロジック層をスパイクする

@Service
public class SeckillService {
@Resource
private SeckillVouchersMapper seckillVouchersMapper;
@Resource
2private VoucherOrdersMapper voucherOrdersMapper;
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;

2: スナップするクーポンを追加する

@Transactional(rollbackFor = Exception.class)
public void addSeckillVouchers(SeckillVouchers seckillVouchers) {
// Non-empty checks
AssertUtil.isTrue(seckillVouchers.getFkVoucherId() == null,"Please select the voucher to be snapped up");
AssertUtil.isTrue(deckillVouchers.getAmount()== 0,"Please enter the total number of vouchers to be snapped up");
Date now = new Date();
AssertUtil.isNotNull(deckillVouchers.getStartTime(),"Please enter the start time");
 
// The following line of code needs to be released for the production environment, commented here for testing purposes
// AssertUtil.isTrue(now.after(deckillVouchers.getStartTime()),"Start time cannot be earlier than current time");
AssertUtil.isNotNull(deckillVouchers.getEndTime(),"Please enter an end time");
AssertUtil.isTrue(now.after(deckillVouchers.getEndTime()),"The end time cannot be earlier than the current time");
AssertUtil.isTrue(deckillVouchers.getStartTime().after(deckillVouchers.getEndTime()),"start time cannot be later than end time");
 
// using Redis implementation
String key= RedisKeyConstant.deckill_vouchers.getKey() + deckillVouchers.getFkVoucherId();
// Verify that the vouchers already exist in Redis for the seconds, hash will not do serialization and deserialization, which is good for performance.
entries(key), fetching the key
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//If not empty or amount inventory>0, the voucher already has a rush, don't create it again.
AssertUtil.isTrue(!map.isEmpty() && (int) map.get("amount") > 0,"the coupon already has a snapping activity");
 
// The snatch activity data is inserted into Redis
seckillVouchers.setIsValid(1);
seckillVouchers.setCreateDate(now);
seckillVouchers.setUpdateDate(now);
//key corresponds to map, use toolset to convert seckillVouchers to map
redisTemplate.opsForHash().putAll(key,BeanUtil.beanToMap(seckillVouchers));
}

3: バウチャーを手に入れる

@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)
{
// Basic parameter verification
AssertUtil.isTrue(voucherId == null || voucherId < 0,"Please select the voucher to be snapped up");
AssertUtil.isNotEmpty(accessToken,"Please login");
 
// using Redis
String key= RedisKeyConstant.deckill_vouchers.getKey() + voucherId;// get map based on key
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//map to object
SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);
 
// Determine whether to start and end
Date now = new Date();
AssertUtil.isTrue(now.before(deckillVouchers.getStartTime()),"The snatch has not yet started");
AssertUtil.isTrue(now.after(deckillVouchers.getEndTime()),"The snag has ended");
 
// Determine if it is sold out
AssertUtil.isTrue(deckillVouchers.getAmount() < 1,"The voucher has been sold out");
 
// Get the login user information
String url = oauthServerName +"user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,accessToken);
if (resultInfo.getCode() ! = ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
 
// The data here is a LinkedHashMap, SignInDinerInfo
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)resultInfo.getData(), new SignInDinerInfo(), false);
 
// determine whether the logged-in user has grabbed (a user can only buy once for this event)
VoucherOrders order = voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),deckillVouchers.getFkVoucherId());
AssertUtil.isTrue(order ! = null,"The user has already grabbed the voucher, no need to grab it again");
 
// deduct inventory, using redis, redis is not set to self-decrease, so to self-decrease, set the step size to -1
long count = redisTemplate.opsForHash().increment(key,"amount",-1);
AssertUtil.isTrue(count < 0,"The coupon has been sold out");
 
// Place the order and store it in the database
VoucherOrders voucherOrders = new VoucherOrders();
voucherOrders.setFkDinerId(dinerInfo.getId());
// No need to maintain foreign key information in Redis
// voucherOrders.setFkSeckillId(deckillVouchers.getId());
voucherOrders.setFkVoucherId(deckillVouchers.getFkVoucherId());
String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
voucherOrders.setOrderNo(orderNo);
voucherOrders.setOrderType(1);
voucherOrders.setStatus(0);
count = voucherOrdersMapper.save(voucherOrders);
AssertUtil.isTrue(count == 0,"User snatch failed");
return ResultInfoUtil.buildSuccess(path,"snagged successfully");
}
} 

5: ポストマンテスト

http://localhost:8083/add

{
"fkVoucherId":1,
"amount":100,
"startTime":"2020-02-04 11:12:00",
"endTime":"2021-02-06 11:12:00"
}

Redisを見る

http://localhost:8083/add を再度実行

6: ストレステスト

redisにある在庫のマイナス値を表示する

redisでの在庫の修正は、まず在庫の値を取得し、次にそれを減算するという2つのプロセスで行われます。そのため、同時実行性が高い状況では、redisがインベントリを差し引く際に問題が発生する可能性があります。これはredisの弱いトランザクションか、luaスクリプトを使って解決することができます。7: Luaをインストールする resources/stock.lua

if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then
  local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));
  if (stock > 0) then
    redis.call('hincrby', KEYS[1], KEYS[2],-1);
    return stock;
  end;
    return 0;
end;

hexists', KEYS[1], KEYS[2]) == 1
hexistsは、キーがredisに存在するかどうかを判断するためのものです。
KEYS[1] は seckill_vouchers:1 KEYS[2] は amount です。
hgetはストックに割り当てる金額を取得します。
hincrbyは自己増分、-1は自己減分です。
redisには自己減算命令がないため、ステップが-1だと自己減算になります。
ここで、lua スクリプトを使用して、redis の在庫照会と在庫減算を単一スレッドのアトミック操作として扱います。

8: Luaの設定

config/RedisTemplateConfiguration.java

@Bean
public DefaultRedisScript<Long> stockScript() {
  DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  // placed in the same level as application.yml
  redisScript.setLocation(new ClassPathResource("stock.lua"));
  redisScript.setResultType(Long.class);
  return redisScript;
} 

9: ビジネスレイヤーの修正

ms-seckill/service/SeckilService.java

1: バウチャーを手に入れる

@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)
{
// Basic parameter verification
AssertUtil.isTrue(voucherId == null || voucherId < 0,"Please select the voucher to be snapped up");
AssertUtil.isNotEmpty(accessToken,"Please login");
// using Redis
String key= RedisKeyConstant.deckill_vouchers.getKey() + voucherId;
// get map based on key
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//map to object
SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);
// Determine whether to start and end
Date now = new Date();AssertUtil.isTrue(now.before(deckillVouchers.getStartTime()),"The snatch has not yet started");
AssertUtil.isTrue(now.after(deckillVouchers.getEndTime()),"The snag has ended");
// Determine if it is sold out
AssertUtil.isTrue(deckillVouchers.getAmount() < 1,"The voucher has been sold out");
// Get the login user information
String url = oauthServerName +"user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,
accessToken);
if (resultInfo.getCode() ! = ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
// The data here is a LinkedHashMap, SignInDinerInfo
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)
resultInfo.getData(), new SignInDinerInfo(), false);
// determine whether the logged-in user has grabbed (a user can only buy once for this activity)
VoucherOrders order = voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),
seckillVouchers.getFkVoucherId());
AssertUtil.isTrue(order ! = null,"The user has already grabbed the voucher, no need to grab it again");
 
// deduct inventory, using redis, redis is not set to self-decrease, so to self-decrease, set the step size to -1
// long count = redisTemplate.opsForHash().increment(key,"amount",-1);
// AssertUtil.isTrue(count < 0,"The coupon has been sold out");
// Place the order and store it in the database
VoucherOrders voucherOrders = new VoucherOrders();
voucherOrders.setFkDinerId(dinerInfo.getId());
// No need to maintain foreign key information in Redis
// voucherOrders.setFkSeckillId(deckillVouchers.getId());
voucherOrders.setFkVoucherId(deckillVouchers.getFkVoucherId());
String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
voucherOrders.setOrderNo(orderNo);
voucherOrders.setOrderType(1);
voucherOrders.setStatus(0);
long count = voucherOrdersMapper.save(voucherOrders);
AssertUtil.isTrue(count == 0,"User snatch failed");
// Use Redis + Lua to solve the problem
// deduct inventory
List<String> keys = new ArrayList<>();
// put the key of redis in keys.add(key);
keys.add("amount");
Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
AssertUtil.isTrue(amount == null || amount < 1,"The coupon has been sold out");
return ResultInfoUtil.buildSuccess(path,"snagged successfully");
} 

10: ストレステスト

redis の在庫を 100 に戻す

ストレステスト

redisでamount=0がマイナスにならないか確認する データベースの注文テーブルt_voucher_ordersで、合計100件の注文が行われたか確認する。

この記事スパイク売れ残りを防ぐためにRedis高同時実行ソースコードソリューションはこちら、より関連するスパイク売れ残りを防ぐためにRedis高同時実行コンテンツはスクリプトハウスの前の記事を検索してくださいまたは次の関連記事を閲覧し続けるあなたは将来的に多くのスクリプトハウスをサポートしています願っています