背景
应用程序升级面临最大挑战是新旧业务切换,将软件从测试的最后阶段带到生产环境,同时要保证系统不间断提供服务。
长期以来,业务升级渐渐形成了几个发布策略:蓝绿发布、灰度发布和滚动发布,目的是尽可能避免因发布导致的流量丢失或服务不可用问题。
本文主要介绍Oinone平台如何实现蓝绿发布。
蓝绿发布:项目逻辑上分为AB组,在项目系统时,首先把A组从负载均衡中摘除,进行新版本的部署。B组仍然继续提供服务。
需求
统一权限
统一登录信息
不同业务数据
实现方案
-
首先需要两个环境并统一流量入口,这里使用Nginx配置负载均衡:nginx如何配置后端服务的负载均衡
-
统一权限配置
在蓝绿环境的覆盖 pro.shushi.pamirs.auth.api.cache.redis.AuthRedisTemplate Bean注册,把setKeySerializer改成不带前缀隔离的类。
package pro.shushi.pamirs.auth.api.cache.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.auth.api.constants.AuthConstants;
import pro.shushi.pamirs.framework.connectors.data.serializer.PamirsStringRedisSerializer;
@Component(AuthConstants.REDIS_TEMPLATE_BEAN_NAME)
public class AuthRedisTemplate<V> extends RedisTemplate<String, V> {
public AuthRedisTemplate(
@Autowired RedisConnectionFactory redisConnectionFactory,
@Autowired PamirsStringRedisSerializer pamirsStringRedisSerializer
) {
this.setConnectionFactory(redisConnectionFactory);
this.setKeySerializer(new PamirsStringRedisSerializer(null));
this.setHashKeySerializer(RedisSerializer.string());
RedisSerializer<Object> valueSerializer = new AuthRedisKryoValueSerializer();
this.setValueSerializer(valueSerializer);
this.setHashValueSerializer(valueSerializer);
}
}
- 统一登录
- 在蓝绿环境自定义实现pro.shushi.pamirs.user.api.spi.UserCacheApi SPI,去除redis前缀
package pro.shushi.pamirs.top.core.spi;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import pro.shushi.pamirs.auth.api.cache.redis.AuthRedisTemplate;
import pro.shushi.pamirs.meta.api.dto.model.PamirsUserDTO;
import pro.shushi.pamirs.meta.api.dto.protocol.PamirsRequestVariables;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.common.spi.SPI;
import pro.shushi.pamirs.user.api.cache.UserCache;
import pro.shushi.pamirs.user.api.configure.UserConfigure;
import pro.shushi.pamirs.user.api.spi.UserCacheApi;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Order(1)
@Component
@SPI.Service("MyUserCache")
public class MyUserCache implements UserCacheApi {
private static final Set<String> DEFAULT_FILTER_URIS = Collections.singleton("/pamirs/message");
@Autowired
private AuthRedisTemplate redisTemplate;
@Override
public PamirsUserDTO getSessionUser(String key) {
String objectValue = getUserCacheAndRenewed(key);
if (StringUtils.isNotBlank(objectValue)) {
return JSON.parseObject(objectValue, PamirsUserDTO.class);
}
return null;
}
@Override
public void setSessionUser(String key, PamirsUserDTO user, Integer expire) {
user.setPassword(null);
expire = getExpire(expire);
redisTemplate.opsForValue().set(key.replace("'", " "), JSON.toJSONString(user), expire, TimeUnit.SECONDS);
// 当前的实现是一个user可以在多个客户端登录,需要在管理端修改user权限后强制清除掉该用户已登录的session,所以需要记录uid对应所有已登录的sessionId
String userRelSessionKey = UserCache.createUserRelSessionKey(user.getUserId());
redisTemplate.opsForSet().add(userRelSessionKey, key);
redisTemplate.expire(userRelSessionKey, expire, TimeUnit.SECONDS);
}
@Override
public void clearSessionUser(String key) {
PamirsUserDTO pamirsUserDTO = getSessionUser(key);
if (null != pamirsUserDTO) {
redisTemplate.opsForSet().remove(UserCache.createUserRelSessionKey(pamirsUserDTO.getUserId()), key);
redisTemplate.delete(key);
}
}
@Override
public void clearSessionUserByUserId(Long userId) {
String cacheKey = UserCache.createUserRelSessionKey(userId);
Set<String> sessionKeySet = redisTemplate.opsForSet().members(cacheKey);
if (sessionKeySet != null) {
sessionKeySet.forEach(sessionKey -> redisTemplate.delete(sessionKey));
redisTemplate.delete(cacheKey);
}
}
protected int getExpire(Integer expire) {
if (expire == null) {
expire = UserConfigure.getDefaultSessionExpire();
}
return expire;
}
protected int getRenewedExpire() {
return UserConfigure.getDefaultSessionRenewedExpire();
}
protected Set<String> getRenewedFilterUrls() {
return Sets.union(UserConfigure.getRenewedFilterUrls(), DEFAULT_FILTER_URIS);
}
protected String getUserCacheAndRenewed(String key) {
String currentUri = null;
if (RequestContextHolder.getRequestAttributes() != null) {
String uri = Optional.ofNullable(PamirsSession.getRequestVariables())
.map(PamirsRequestVariables::getURI)
.map(URI::getPath)
.orElse(null);
if (StringUtils.isNotBlank(uri)) {
currentUri = uri;
}
}
if (StringUtils.isNotBlank(currentUri) && getRenewedFilterUrls().contains(currentUri)) {
return redisTemplate.opsForValue().get(key).toString();
}
int ttl = getRenewedExpire();
if (ttl <= 0) {
return redisTemplate.opsForValue().get(key).toString();
}
List<Object> result = redisTemplate.executePipelined(new SessionCallback<Void>() {
@SuppressWarnings({"unchecked", "NullableProblems"})
@Override
public Void execute(RedisOperations operations) throws DataAccessException {
operations.opsForValue().get(key);
operations.expire(key, ttl, TimeUnit.SECONDS);
return null;
}
});
if (result.size() >= 1) {
return (String) result.get(0);
}
return null;
}
}
- 在蓝绿环境的yml配置文件里指定使用 MyUserCache SPI
pamirs:
user:
session:
mode:
MyUserCache
Oinone社区 作者:yexiu原创文章,如若转载,请注明出处:https://doc.oinone.top/dai-ma-shi-jian/22107.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验