利用Redis的setIfAbsent避免因并发问题导致重复发送邮件

场景

站点同步场景,如果判断是该站点所属的运营商是新增的,则需要针对该运营商,给运营人员发送一次邮件通知,每天仅需发送即可。次日凌晨,清除当前邮件已发送的标记。

方案

  • 利用setIfAbsent(如果不存在,则写入并返回true,否则不写并返回false)
  • 利用scan,返回全部新增运营商的KEY,便于批量删除邮件已发送的标记

代码示例

package tech.foolfish.demo.service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

@Service
public class EmailService {

    private static final String REDIS_KEY_EMAIL_SENT_EXCEPTIONAL_OPERATOR = "EMAIL_SENT_EXCEPTIONAL_OPERATOR:";

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 统计周期内,未发送过邮件
     * 
     * @param operatorId
     * @return
     */
    public boolean noEmailSent(String operatorId) {
        return redisTemplate.opsForValue().setIfAbsent(REDIS_KEY_EMAIL_SENT_EXCEPTIONAL_OPERATOR + operatorId, "true");
    }

    /**
     * 统计周期内,发送过邮件
     * 
     * @param operatorId
     * @return
     */
    public boolean emailSent(String operatorId) {
        return !noEmailSent(operatorId);
    }

    /**
     * 统计周期内,打标记:该运营商刚发送过一次邮件
     * 
     * @param operatorId
     */
    public void markEmailSent(String operatorId) {
        redisTemplate.opsForValue().setIfAbsent(REDIS_KEY_EMAIL_SENT_EXCEPTIONAL_OPERATOR + operatorId, "true");
    }

    /**
     * 统计周期内,清除标记
     */
    public void clearMarkOfEmailSent() {
        String redisKey = REDIS_KEY_EMAIL_SENT_EXCEPTIONAL_OPERATOR + "*";
        deleteKey(redisKey);
    }

    /**
     * 模糊清除KEY
     * 
     * @param pattern
     */
    private void deleteKey(String pattern) {
        List<String> keys = getKeys(pattern);
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        redisTemplate.delete(keys);
    }

    private List<String> getKeys(String pattern) {
        List<String> keys = new ArrayList<>();
        this.scan(pattern, item -> {
            String key = new String(item, StandardCharsets.UTF_8);
            keys.add(key);
        });
        return keys;
    }

    private void scan(String pattern, Consumer<byte[]> consumer) {
        redisTemplate.execute((RedisConnection connection) -> {
            try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
                cursor.forEachRemaining(consumer);
                return null;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

}