开源工具(一)Redis事务回滚

6 篇文章 1 订阅

一、引言

        github地址:GitHub - SongTing0711/redis-transaction        

        开发过程中redis与db的数据不一致经常令人头痛,因为在Mysql遇到异常回滚之后redis不会变化,对业务功能产生影响,本文基于AOP和栈开发redis的事务回滚机制,保证数据一致性!

        这里有技术人员咨询过博主,为什么不使用异步写的操作,异步写存在延时情况,当业务数据存在强一致性的场景就不适合异步写。

        还有人咨询为什么不使用redis提供的事务命令+管道命令进行一次性发送?

1、这里要知道redis的事务命令是不具备持久性的,其实就是放在入口处的队列命令,在《Redis深度历险》中有详细介绍,所以这种方式和使用本地ThreadLocal的风险是一样的

2、另外这种相当于把压力全部给到了redis,而一个redis集群往往不是只服务于一个系统,很容易导致redis的波动,而且还不能优化,因为压力是来自各方的

3、一串redis操作在最后一起发出,这个非常不利于代码解耦,后续的维护也会困难

        基于以上种种考虑,博主开发了基于补偿回滚策略的redis回滚工具。

二、RedisTransaction组件

1、注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisTransaction {
    //是否自动清除RedisTransactionUtil
    boolean atuoRemove() default false;
    //如果使用RedisTemplate或者StringRedisTemplate操作集合删除,需要设置
    long index() default 0l;
}

2、工具类

①操作类型枚举

        开发项目中对于redis的使用场景基本是存储、查询、删除,对于数据一致性会产生影响的就是存储和删除。

@Getter
public enum RedisOperateTypeEnum {

    STRING_SET("string", "add"),
    STRING_DELETE("string", "delete"),
    LIST_ADD("list", "add"),
    LIST_REMOVE("list", "remove"),
    ;

    private String dataType;
    private String operateType;

    RedisOperateTypeEnum(String key, String value) {
        this.dataType = key;
        this.operateType = value;
    }
}

public enum RedisClientTypeEnum {

    REDISSON("redisson"),
    REDIS_TEMPLATE("redisTemplate"),
    STRING_REDIS_TEMPLATE("stringRedisTemplate"),
    ;

    private String redisClientType;

    RedisClientTypeEnum(String redisClientType) {
        this.redisClientType = redisClientType;
    }
}

②存储实体

@Data
public class RedisOperateUtil<T> {

    private RedisOperateTypeEnum operateTypeEnum;
    private String key;
    private T value;
    private T prevValue;
}

 ③ThreadLocal工具类

public class RedisTransactionUtil {

    static ThreadLocal<Stack> TRANSACTION_QUEUE = new ThreadLocal<Stack>();

    public static void add(RedisOperateUtil redisOperateUtil) {
        if (Objects.isNull(TRANSACTION_QUEUE.get())) {
            Stack stack = new Stack();
            TRANSACTION_QUEUE.set(stack);
        }
        TRANSACTION_QUEUE.get().add(redisOperateUtil);
    }
    public static Stack get() {
        return TRANSACTION_QUEUE.get();
    }
    public static void remove() {
        TRANSACTION_QUEUE.remove();
    }
}

3、切面

        这里展示了String的处理,其他redis数据类型操作原理相同,都是根据栈中存储的redis操作进行逆序反向处理。

package com.core;

import com.annotation.RedisTransaction;
import com.util.RedisTransactionCacheUtils;
import com.util.RedisOperateUtil;
import com.util.RedisTransactionUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.Stack;

/**
 * redis事务,切面处理类
 */
@Slf4j
@Aspect
@Component
public class RedisTransactionAspect {

    @Resource
    private RedisTransactionCacheUtils redisTransactionCacheUtils;

    @Pointcut("@annotation(com.annotation.RedisTransaction)")
    public void transactionPointCut() {
    }

    @AfterReturning("transactionPointCut() && @annotation(redisTransaction)")
    public void afterReturning(RedisTransaction redisTransaction) {
        //如果方法注解中开启自动清除,就去除
        if (redisTransaction.atuoRemove()) {
            RedisTransactionUtil.remove();
            log.info("自动清除RedisTransactionUtil:{}", Thread.currentThread().getName());
        }
    }

    @AfterThrowing("transactionPointCut() && @annotation(redisTransaction)")
    public void afterThrowing(RedisTransaction redisTransaction) {
        try {
            Stack<RedisOperateUtil> stack = RedisTransactionUtil.get();
            if (Objects.isNull(stack)) {
                return;
            }
            log.info("redis回滚:{}", stack);
            RedisOperateUtil redisOperateUtil;
            while (!stack.isEmpty()) {
                redisOperateUtil = stack.pop();
                switch (redisOperateUtil.getOperateTypeEnum()) {
                    case STRING_SET:
                        if (Objects.nonNull(redisOperateUtil.getPrevValue())) {
                            redisTransactionCacheUtils.stringAdd(redisOperateUtil.getKey(),redisOperateUtil.getPrevValue());
                            break;
                        }
                        redisTransactionCacheUtils.stringDelete(redisOperateUtil.getKey());
                        break;
                    case STRING_DELETE:
                        redisTransactionCacheUtils.stringAdd(redisOperateUtil.getKey(), redisOperateUtil.getValue());
                        break;
                    case LIST_ADD:
                        redisTransactionCacheUtils.listRemove(redisOperateUtil.getKey(), redisOperateUtil.getValue(),redisTransaction.index());
                        break;
                    case LIST_REMOVE:
                        redisTransactionCacheUtils.listAdd(redisOperateUtil.getKey(), redisOperateUtil.getValue());
                        break;
                    default:
                        break;
                }
            }
        } catch (Exception e) {
            log.error("reidis回滚失败:{}", e);
        } finally {
            //如果方法注解中开启自动清除,就去除
            if (redisTransaction.atuoRemove()) {
                RedisTransactionUtil.remove();
                log.info("自动清除RedisTransactionUtil:{}", Thread.currentThread().getName());
            }
        }
    }

}

4、redis包装

@Slf4j
@Component
public class RedisTransactionCacheUtils<V> {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private void setRedisTransaction(String k, V v, V prev, RedisOperateTypeEnum redisOperateTypeEnum) {
        RedisOperateUtil redisOperateUtil = new RedisOperateUtil();
        redisOperateUtil.setOperateTypeEnum(redisOperateTypeEnum);
        redisOperateUtil.setKey(k);
        redisOperateUtil.setPrevValue(prev);
        redisOperateUtil.setValue(v);
        RedisTransactionUtil.add(redisOperateUtil);
    }

    /**
     * list
     *
     * @return true 成功 false 失败
     */
    public boolean listAddTransaction(String k, V v) {
        try {
            if (v != null) {
                switch (RedisTransactionCommonUtil.getRedisClient()) {
                    case REDISSON:
                        RList<V> list = redissonClient.getList(k);
                        if (!list.contains(v)) {
                            list.add(v);
                            this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_ADD);
                        }
                        break;
                    case REDIS_TEMPLATE:
                        redisTemplate.opsForList().rightPush(k, v);
                        this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_ADD);
                        break;
                    case STRING_REDIS_TEMPLATE:
                        stringRedisTemplate.opsForList().rightPush(k, (String) v);
                        this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_ADD);
                        break;
                    default:
                        throw new RedisTrancactionException("unknown redis client");
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            log.warn("redis setList fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * list
     *
     * @return true 成功 false 失败
     */
    public boolean listAdd(String k, V v) {
        try {
            if (v != null) {
                switch (RedisTransactionCommonUtil.getRedisClient()) {
                    case REDISSON:
                        RList<V> list = redissonClient.getList(k);
                        if (!list.contains(v)) {
                            list.add(v);
                        }
                        break;
                    case REDIS_TEMPLATE:
                        redisTemplate.opsForList().rightPush(k, v);
                        break;
                    case STRING_REDIS_TEMPLATE:
                        stringRedisTemplate.opsForList().rightPush(k, (String) v);
                        break;
                    default:
                        throw new RedisTrancactionException("unknown redis client");
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            log.warn("redis setList fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }


    /**
     * list
     *
     * @return true 成功 false 失败
     */
    public boolean listRemoveTransaction(String k, V v, Long index) {
        try {
            if (v != null) {
                switch (RedisTransactionCommonUtil.getRedisClient()) {
                    case REDISSON:
                        RList<V> list = redissonClient.getList(k);
                        if (!list.contains(v)) {
                            list.remove(v);
                            this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_REMOVE);
                        }
                        break;
                    case REDIS_TEMPLATE:
                        redisTemplate.opsForList().remove(k, index, v);
                        this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_REMOVE);
                        break;
                    case STRING_REDIS_TEMPLATE:
                        stringRedisTemplate.opsForList().rightPush(k, (String) v);
                        this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.LIST_REMOVE);
                        break;
                    default:
                        throw new RedisTrancactionException("unknown redis client");
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            log.warn("redis deleteFromList fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * list
     *
     * @return true 成功 false 失败
     */
    public boolean listRemove(String k, V v, Long index) {
        try {
            if (v != null) {
                switch (RedisTransactionCommonUtil.getRedisClient()) {
                    case REDISSON:
                        RList<V> list = redissonClient.getList(k);
                        if (!list.contains(v)) {
                            list.remove(v);
                        }
                        break;
                    case REDIS_TEMPLATE:
                        redisTemplate.opsForList().remove(k, index, v);
                        break;
                    case STRING_REDIS_TEMPLATE:
                        stringRedisTemplate.opsForList().rightPush(k, (String) v);
                        break;
                    default:
                        throw new RedisTrancactionException("unknown redis client");
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            log.warn("redis deleteFromList fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * list
     *
     * @return true 成功 false 失败
     */
    public boolean stringSetExpireTransaction(String k, V v, Long timeOut, TimeUnit timeUnit) {
        try {
            if (v == null) {
                return false;
            }
            V prev = null;
            switch (RedisTransactionCommonUtil.getRedisClient()) {
                case REDISSON:
                    RBucket<V> bucket = redissonClient.getBucket(k);
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = bucket.get();
                    }
                    bucket.set(v);
                    bucket.expire(timeOut, timeUnit);
                    break;
                case REDIS_TEMPLATE:
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = (V) redisTemplate.opsForValue().get(k);
                    }
                    redisTemplate.opsForValue().set(k, v);
                    break;
                case STRING_REDIS_TEMPLATE:
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = (V) stringRedisTemplate.opsForValue().get(k);
                    }
                    stringRedisTemplate.opsForValue().set(k, (String) v);
                    break;
                default:
                    throw new RedisTrancactionException("unknown redis client");
            }
            this.setRedisTransaction(k, v, prev, RedisOperateTypeEnum.STRING_SET);
            return true;
        } catch (Exception e) {
            log.warn("redis setListExpire fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * v
     *
     * @return true 成功 false 失败
     */
    public boolean stringSetTransaction(String k, V v) {
        try {
            if (v == null) {
                return false;
            }
            V prev = null;
            switch (RedisTransactionCommonUtil.getRedisClient()) {
                case REDISSON:
                    RBucket<V> bucket = redissonClient.getBucket(k);
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = bucket.get();
                    }
                    bucket.set(v);
                    break;
                case REDIS_TEMPLATE:
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = (V) redisTemplate.opsForValue().get(k);
                    }
                    redisTemplate.opsForValue().set(k, v);
                    break;
                case STRING_REDIS_TEMPLATE:
                    if (RedisTransactionCommonUtil.getQueryPrev()) {
                        prev = (V) stringRedisTemplate.opsForValue().get(k);
                    }
                    stringRedisTemplate.opsForValue().set(k, (String) v);
                    break;
                default:
                    throw new RedisTrancactionException("unknown redis client");
            }
            this.setRedisTransaction(k, v, prev, RedisOperateTypeEnum.STRING_SET);
            return true;
        } catch (Exception e) {
            log.warn("redis setValue fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * v
     *
     * @return true 成功 false 失败
     */
    public boolean stringAdd(String k, V v) {
        try {
            if (v == null) {
                return false;
            }
            switch (RedisTransactionCommonUtil.getRedisClient()) {
                case REDISSON:
                    RBucket<V> bucket = redissonClient.getBucket(k);
                    bucket.set(v);
                    break;
                case REDIS_TEMPLATE:
                    redisTemplate.opsForValue().set(k, v);
                    break;
                case STRING_REDIS_TEMPLATE:
                    stringRedisTemplate.opsForValue().set(k, (String) v);
                    break;
                default:
                    throw new RedisTrancactionException("unknown redis client");
            }
            return true;
        } catch (Exception e) {
            log.warn("redis setValue fail, key = {}, value = {}, e = {}", k, v, e);
            return false;
        }
    }

    /**
     * v
     *
     * @return true 成功 false 失败
     */
    public boolean stringDeleteTransaction(String k, V v) {
        try {
            switch (RedisTransactionCommonUtil.getRedisClient()) {
                case REDISSON:
                    RBucket<V> bucket = redissonClient.getBucket(k);
                    if (bucket.isExists()) {
                        bucket.delete();
                    }
                    break;
                case REDIS_TEMPLATE:
                    redisTemplate.opsForValue().getAndDelete(k);
                    break;
                case STRING_REDIS_TEMPLATE:
                    stringRedisTemplate.opsForValue().getAndDelete(k);
                    break;
                default:
                    throw new RedisTrancactionException("unknown redis client");
            }
            this.setRedisTransaction(k, v, null, RedisOperateTypeEnum.STRING_DELETE);
            return true;
        } catch (Exception e) {
            log.warn("redis deleteValue fail, key = {}, value = {}, e = {}", k, e);
            return false;
        }
    }

    /**
     * v
     *
     * @return true 成功 false 失败
     */
    public boolean stringDelete(String k) {
        try {
            switch (RedisTransactionCommonUtil.getRedisClient()) {
                case REDISSON:
                    RBucket<V> bucket = redissonClient.getBucket(k);
                    if (bucket.isExists()) {
                        bucket.delete();
                    }
                    break;
                case REDIS_TEMPLATE:
                    redisTemplate.opsForValue().getAndDelete(k);
                    break;
                case STRING_REDIS_TEMPLATE:
                    stringRedisTemplate.opsForValue().getAndDelete(k);
                    break;
                default:
                    throw new RedisTrancactionException("unknown redis client");
            }
            return true;
        } catch (Exception e) {
            log.warn("redis deleteValue fail, key = {}, value = {}, e = {}", k, e);
            return false;
        }
    }

}

 三、使用

1、目前支持链接redis的工具主要是Redisson、RedisTemplate、StringRedisTemplate,默认Redisson

2、进行回滚时需要考虑是否要进行查询前镜像,可以通过设置RedisTransactionCommonUtil的QUERY_PREV属性

3、需要使用RedisTransactionCacheUtils操作需要回滚的redis数据,此时加入threadlocal
@Resource
private RedisTransactionCacheUtils redisTransactionCacheUtils;

@RedisTransaction(atuoRemove = true)
public void shopAdd(ShopOnlineDTO request) {
    //业务处理
    redisTransactionCacheUtils.stringSetTransaction(request.getShopId(),request.getPrice());
    //业务处理
    redisTransactionCacheUtils.stringSetTransaction(request.getShopId(),request.getLocation());
    //业务处理
}

设置redis客户端为RedisTemplate
RedisTransactionCommonUtil.setRedisClient(RedisClientTypeEnum.REDIS_TEMPLATE);

设置查询前镜像
RedisTransactionCommonUtil.setQueryPrev(true);

四、总结

        作者在线上使用过程中避免了不少数据不一致,感兴趣的小伙伴可以试试。

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖当当技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值