Skip to content

Latest commit

 

History

History
188 lines (157 loc) · 6.42 KB

策略模式.md

File metadata and controls

188 lines (157 loc) · 6.42 KB

在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等

场景一

我们需要画一个图形,可选的策略就是用红色笔来画,还是绿色笔来画,或者蓝色笔来画。

首先,先定义一个策略接口:

public interface IStrategyService {
    void draw(int radius, int x, int y);
}

然后我们定义具体的几个策略:

// 策略一
public class RedPenImpl implements IStrategyService {
    @Override
    public void draw(int radius, int x, int y) {
        System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
    }
}
// 策略二
public class GreenPenImpl implements IStrategyService {
    @Override
    public void draw(int radius, int x, int y) {
        System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
    }
}
// 策略三
public class BluePenImpl implements IStrategyService {
    @Override
    public void draw(int radius, int x, int y) {
        System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
    }
}

使用策略的类:

public class StrategyContext {
    private IStrategyService strategy;

    public StrategyContext(IStrategyService strategy){
        this.strategy = strategy;
    }

    public void executeDraw(int radius, int x, int y){
         strategy.draw(radius, x, y);
    }
}

测试类:

public class StrategyDemoTest {
    public static void main(String[] args) {
        StrategyContext context = new StrategyContext(new BluePenImpl()); // 使用蓝色笔来画
        context.executeDraw(10, 0, 0);
    }
}
放到一张图上,让大家看得清晰些:

image

具体应用

我们在为rocketMQ做消息幂等的时候,有两种方案,一种是用redis做幂等,另一种用mysql做幂等,大概代码如下

public interface IPersist {

     String CONSUME_STATUS_CONSUMING = "CONSUMING";
     String CONSUME_STATUS_CONSUMED = "CONSUMED";

    boolean setConsumingIfNX(DedupElement dedupElement, long dedupProcessingExpireMilliSeconds);
}

Mysql的策略如下:

public class JDBCPersit implements IPersist {
    private final JdbcTemplate jdbcTemplate;

    public JDBCPersit(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public boolean setConsumingIfNX(DedupElement dedupElement, long dedupProcessingExpireMilliSeconds) {
        long expireTime = System.currentTimeMillis() + dedupProcessingExpireMilliSeconds;
        try {
            int  i = jdbcTemplate.update("INSERT INTO t_rocketmq_dedup(application_name, topic, tag, msg_uniq_key, status, expire_time) values (?, ?, ?, ?, ?, ?)",dedupElement.getApplication(), dedupElement.getTopic(), dedupElement.getTag(), dedupElement.getMsgUniqKey(), CONSUME_STATUS_CONSUMING, expireTime);
        } catch (org.springframework.dao.DuplicateKeyException e) {
            log.warn("found consuming/consumed record, set setConsumingIfNX fail {}", dedupElement);
            // 由于mysql不支持消息过期,出现重复主键的情况下,有可能是过期的一些记录,这里动态的删除这些记录后重试
            int  i = delete(dedupElement, true);
            if (i > 0) {//如果删除了过期消息
                log.info("delete {} expire records, now retry setConsumingIfNX again", i);
                return setConsumingIfNX(dedupElement, dedupProcessingExpireMilliSeconds);
            } else {
                return false;
            }
        } catch (Exception e) {
            log.error("unknown error when jdbc insert, will consider success", e);
            return true;
        }
        //插入成功则返回true
        return true;
    }
}

Redis策略如下:

public class RedisPersist implements IPersist {

    private final StringRedisTemplate redisTemplate;

    public RedisPersist(StringRedisTemplate redisTemplate) {
        if (redisTemplate == null) {
            throw new NullPointerException("redis template is null");
        }
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean setConsumingIfNX(DedupElement dedupElement, long dedupProcessingExpireMilliSeconds) {
        String dedupKey = buildDedupMessageRedisKey(dedupElement.getApplication(), dedupElement.getTopic(), dedupElement.getTag(), dedupElement.getMsgUniqKey());

        //setnx, 成功就可以消费
        Boolean execute = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.set(dedupKey.getBytes(),
                (CONSUME_STATUS_CONSUMING).getBytes(), Expiration.milliseconds(dedupProcessingExpireMilliSeconds),
                RedisStringCommands.SetOption.SET_IF_ABSENT));
        if (execute == null) {
            return false;
        }
        return execute;
    }
}

使用策略类:

public class DedupConfig {

    private IPersist persist;

    private DedupConfig(String applicationName, int dedupStrategy, StringRedisTemplate redisTemplate) {
        if (redisTemplate != null) {
            this.persist = new RedisPersist(redisTemplate);
        }
    }

    private DedupConfig(String applicationName, int dedupStrategy, JdbcTemplate jdbcTemplate) {
        if (jdbcTemplate != null) {
            this.persist = new JDBCPersit(jdbcTemplate);
        }
    }


    /**
     * 利用redis去重
     * @param applicationName 应用名称
     * @param redisTemplate redis连接
     * @return
     */
    public static DedupConfig enableDedupConsumeConfig(String applicationName, StringRedisTemplate redisTemplate) {
        return new DedupConfig(applicationName, DEDUP_STRATEGY_CONSUME_LATER, redisTemplate);
    }

    /**
     * 利用mysql去重
     * @param applicationName 应用名称
     * @param jdbcTemplate 数据库连接
     * @return
     */
    public static DedupConfig enableDedupConsumeConfig(String applicationName, JdbcTemplate jdbcTemplate) {
        return new DedupConfig(applicationName, DEDUP_STRATEGY_CONSUME_LATER, jdbcTemplate);
    }

    public static DedupConfig disableDupConsumeConfig(String applicationName) {
        return new DedupConfig(applicationName);
    }
}