分享程式代碼相關筆記
目前文章總數:172 篇
最後更新:2025年 03月 22日
通常 Web 網站,會有 3 個部分:
1. 用戶連線 | : | 用戶訪問網站時,瀏覽器向伺服器發送請求,伺服器接收請求並返回響應。這通常涉及 HTTP 請求和響應。 |
2. 建立 Session | : | 伺服器為每個用戶創建一個 Session,用於存儲用戶的臨時數據,如登入狀態、用戶偏好等。這些數據保存在伺服器端,並與該用戶的會話綁定。 |
3. 用戶 Cookie | : | 伺服器會向用戶的瀏覽器發送一個 Cookie,這通常包含一個 Session ID,讓伺服器能夠識別後續請求來自同一個用戶。 |
但是伺服器需要更新時、Docker Container 刪除重建後,伺服器的 Session 就會遺失,導致用戶登入狀態不見。
當啟動範例專案,並且輸入金額 120 並更新
※指尚未實作 Redis 持久化 Session 前的代碼
這時會更新成 120
這時關閉程式,再重新啟動,剛剛的 Session 就會遺失歸 0
以架構來看,可以知道解決方案的關鍵在 Session 保存時一併寫到 Redis 中
並且當取 Session 時,若無法取得 Sessoin 則從 Redis 取得。
這部分 Redis.StackExchange 套件與 Asp.net Core 已整合得很好,可以快速實現
打開範例專案:Redis 持久化 Session 範例代碼,架構基本分成以下:
1. Service | : | 實現 Reids + Session 的方法、更新餘額的商業邏輯 |
2. Web控制器 | : | 提供前端呼叫的 API、檢視 |
3. Html 畫面 | : | 使用 JQuery 呼叫 API,進行取餘額、更新餘額 |
4. Redis 配置 | : | Redis 的連線位置 |
5. 初始化配置 | : | 依賴注入相關的Service ,並且啟用 Redis 與 Session 的綁定 |
實現 Session + Redis 雙寫入的泛型方法
完成取得、更新、刪除 三種實作
public class CacheService : ICacheService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDistributedCache _cache;
private readonly double _expireTime = 1.5;
public CacheService(
IHttpContextAccessor httpContextAccessor,
IDistributedCache cache)
{
_httpContextAccessor = httpContextAccessor;
_cache = cache;
}
/// <summary>
/// 1. 保存 Session、Redis
/// </summary>
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
{
var jsonData = JsonConvert.SerializeObject(value);
// 存到 Session
_httpContextAccessor.HttpContext?.Session.SetString(key, jsonData);
// 存到 Redis
await _cache.SetStringAsync(key, jsonData, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiry ?? TimeSpan.FromHours(_expireTime)
});
}
/// <summary>
/// 2. 讀取 Session、Redis
/// </summary>
public async Task<T> GetAsync<T>(string key)
{
// 先從 Session 取
var sessionData = _httpContextAccessor.HttpContext?.Session.GetString(key);
if (!string.IsNullOrEmpty(sessionData))
{
return JsonConvert.DeserializeObject<T>(sessionData);
}
// 從 Redis 取
var redisData = await _cache.GetStringAsync(key);
return string.IsNullOrEmpty(redisData)
? default(T)
: JsonConvert.DeserializeObject<T>(redisData);
}
/// <summary>
/// 3. 移除 Session, Redis 資料
/// </summary>
public async Task RemoveAsync(string key)
{
await _cache.RemoveAsync(key);
}
}
管理用戶餘額的商業邏輯,核心在於管理自己的方法名稱,檢核
public class UserBalanceService : IUserBalanceService
{
private readonly ICacheService _cacheService;
private readonly string _balanceName = "Balance_";
public UserBalanceService(ICacheService cacheService)
{
_cacheService = cacheService;
}
/// <summary>
/// 更新餘額
/// </summary>
public async Task UpdateBalance(int userId, decimal amount)
{
await _cacheService.SetAsync($"{_balanceName}{userId}", amount);
}
/// <summary>
/// 取得當前餘額
/// </summary>
public async Task<decimal> GetBalance(int userId, decimal amount)
{
return await _cacheService.GetAsync<decimal>($"{_balanceName}{userId}");
}
}
提供呼叫更新餘額、取得餘額 API
※範例關係直接使用 Get 有傳遞參數,且重要資料仍建議使用 POST
public IActionResult Index()
{
return View();
}
/// <summary>
/// 1. 更新用戶金額
/// </summary>
[HttpGet]
public async Task<IActionResult> UpdateBalance(decimal amount)
{
await _userBalance.UpdateBalance(1, amount);
return Ok();
}
/// <summary>
/// 2. 取得用戶金額
/// </summary>
[HttpGet]
public async Task<IActionResult> GetBalance(decimal amount)
{
var currentAmount = await _userBalance.GetBalance(1, amount);
return Ok(currentAmount);
}
呼叫 API 更新餘額、取得餘額
@{
ViewData["Title"] = "Session 持久化";
}
<!DOCTYPE html>
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h3>用戶餘額管理</h3>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col">
<h4>當前餘額:<span id="currentBalance">0</span></h4>
</div>
</div>
<div class="row">
<div class="col">
<div class="input-group mb-3">
<input type="number" id="amountInput" class="form-control" placeholder="請輸入金額">
<button class="btn btn-primary" type="button" id="updateBtn">更新金額</button>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
$(document).ready(function () {
// 載入時取得當前餘額
getBalance();
// 更新按鈕點擊事件
$("#updateBtn").click(function () {
const amount = $("#amountInput").val();
if (!amount) {
alert("請輸入金額");
return;
}
// 呼叫更新金額 API
updateBalance(parseFloat(amount));
});
});
// 取得餘額
function getBalance() {
$.ajax({
url: '@Url.Action("GetBalance", "Balance")',
type: 'GET',
data: { amount: 0 },
success: function (response) {
$("#currentBalance").text(response);
},
error: function () {
alert("取得餘額失敗");
}
});
}
// 更新餘額
function updateBalance(amount) {
$.ajax({
url: '@Url.Action("UpdateBalance", "Balance")',
type: 'GET',
data: { amount: amount },
success: function () {
alert("更新成功");
getBalance(); // 重新取得餘額
$("#amountInput").val(''); // 清空輸入框
},
error: function () {
alert("更新失敗");
}
});
}
</script>
}
取得 Redis 連線字串 RedisConnection
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RedisConnection": "127.0.0.1:6379,abortConnect=False,connectRetry=3,connectTimeout=3000,defaultDatabase=1,syncTimeout=3000,responseTimeout=3000"
}
}
關鍵在 2. 配置 Session + Redis 這段代碼實現了 Redis + Session 的整合。
後續呼叫 .SetStringAsync() 方法操作都會寫入到 Redis 中
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// 1. 取得 Redis 連線配置
var redisConnection = builder.Configuration.GetConnectionString("RedisConnection");
// 2. 配置 Session + Redis
// ※ Nuget 安裝 => Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnection;
options.InstanceName = "MySession_";
});
// 3. 配置 Session 初始配置
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1.5);
options.Cookie.IsEssential = true;
});
// 4. 註冊 HttpContextAccessor
builder.Services.AddHttpContextAccessor();
// 5. 依賴性注入相關 Scope Singleton
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<IUserBalanceService, UserBalanceService>();
var app = builder.Build();
// 6. 應用程式啟用 Session
app.UseSession();
//... 略
app.Run();
再次啟動程式後,輸入金額 2000 ,然後重啟機器,可以發現金額不變
實際上 Redis 中已經保存好此 Session 只要不到期(Session 與 Redis),用戶的瀏覽器都可以抓到該值