一、引言
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);
四、总结
作者在线上使用过程中避免了不少数据不一致,感兴趣的小伙伴可以试试。