淺談C#在網(wǎng)絡(luò)波動(dòng)時(shí)防重復(fù)提交的方法
前幾天,公司數(shù)據(jù)庫(kù)出現(xiàn)了兩條相同的數(shù)據(jù),而且時(shí)間相同(毫秒也相同)。排查原因,發(fā)現(xiàn)是網(wǎng)絡(luò)波動(dòng)造成了重復(fù)提交。
由于網(wǎng)絡(luò)波動(dòng)而重復(fù)提交的例子也比較多:

網(wǎng)絡(luò)上,防重復(fù)提交的方法也很多,使用redis鎖,代碼層面使用lock。
但是,我沒(méi)有發(fā)現(xiàn)一個(gè)符合我心意的解決方案。因?yàn)榫W(wǎng)上的解決方案,第一次提交返回成功,第二次提交返回失敗。由于兩次返回信息不一致,一次成功一次失敗,我們不確定客戶端是以哪個(gè)返回信息為準(zhǔn),雖然我們希望客戶端以第一次返回成功的信息為準(zhǔn),但客戶端也可能以第二次失敗信息運(yùn)行,這是一個(gè)不確定的結(jié)果。
在重復(fù)提交后,如果客戶端的接收到的信息都相同,都是成功,那客戶端就可以正常運(yùn)行,就不會(huì)影響用戶體驗(yàn)。
我想到一個(gè)緩存類,來(lái)源于PetaPoco。
Cache<TKey, TValue>代碼如下:
public class Cache<TKey, TValue>
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly Dictionary<TKey, TValue> _map = new Dictionary<TKey, TValue>();
public int Count {
get { return _map.Count; }
}
public TValue Execute(TKey key, Func<TValue> factory)
{
// Check cache
_lock.EnterReadLock();
TValue val;
try {
if (_map.TryGetValue(key, out val))
return val;
} finally {
_lock.ExitReadLock();
}
// Cache it
_lock.EnterWriteLock();
try {
// Check again
if (_map.TryGetValue(key, out val))
return val;
// Create it
val = factory();
// Store it
_map.Add(key, val);
// Done
return val;
} finally {
_lock.ExitWriteLock();
}
}
public void Clear()
{
// Cache it
_lock.EnterWriteLock();
try {
_map.Clear();
} finally {
_lock.ExitWriteLock();
}
}
}
Cache<TKey, TValue>符合我的要求,第一次運(yùn)行后,會(huì)將值緩存,第二次提交會(huì)返回第一次的值。
但是,細(xì)細(xì)分析Cache<TKey, TValue> 類,可以發(fā)現(xiàn)有以下幾個(gè)缺點(diǎn)
1、 不會(huì)自動(dòng)清空緩存,適合一些key不多的數(shù)據(jù),不適合做為網(wǎng)絡(luò)接口。
2、 由于_lock.EnterWriteLock,多線程會(huì)變成并單線程,不適合做為網(wǎng)絡(luò)接口。
3、 沒(méi)有過(guò)期緩存判斷。
于是我對(duì)Cache<TKey, TValue>進(jìn)行改造。
AntiDupCache代碼如下:
/// <summary>
/// 防重復(fù)緩存
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public class AntiDupCache<TKey, TValue>
{
private readonly int _maxCount;//緩存最高數(shù)量
private readonly long _expireTicks;//超時(shí) Ticks
private long _lastTicks;//最后Ticks
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();
private readonly Dictionary<TKey, Tuple<long, TValue>> _map = new Dictionary<TKey, Tuple<long, TValue>>();
private readonly Dictionary<TKey, AntiDupLockSlim> _lockDict = new Dictionary<TKey, AntiDupLockSlim>();
private readonly Queue<TKey> _queue = new Queue<TKey>();
class AntiDupLockSlim : ReaderWriterLockSlim { public int UseCount; }
/// <summary>
/// 防重復(fù)緩存
/// </summary>
/// <param name="maxCount">緩存最高數(shù)量,0 不緩存,-1 緩存所有</param>
/// <param name="expireSecond">超時(shí)秒數(shù),0 不緩存,-1 永久緩存 </param>
public AntiDupCache(int maxCount = 100, int expireSecond = 1)
{
if (maxCount < 0) {
_maxCount = -1;
} else {
_maxCount = maxCount;
}
if (expireSecond < 0) {
_expireTicks = -1;
} else {
_expireTicks = expireSecond * TimeSpan.FromSeconds(1).Ticks;
}
}
/// <summary>
/// 個(gè)數(shù)
/// </summary>
public int Count {
get { return _map.Count; }
}
/// <summary>
/// 執(zhí)行
/// </summary>
/// <param name="key">值</param>
/// <param name="factory">執(zhí)行方法</param>
/// <returns></returns>
public TValue Execute(TKey key, Func<TValue> factory)
{
// 過(guò)期時(shí)間為0 則不緩存
if (object.Equals(null, key) || _expireTicks == 0L || _maxCount == 0) { return factory(); }
Tuple<long, TValue> tuple;
long lastTicks;
_lock.EnterReadLock();
try {
if (_map.TryGetValue(key, out tuple)) {
if (_expireTicks == -1) return tuple.Item2;
if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
}
lastTicks = _lastTicks;
} finally { _lock.ExitReadLock(); }
AntiDupLockSlim slim;
_slimLock.EnterUpgradeableReadLock();
try {
_lock.EnterReadLock();
try {
if (_lastTicks != lastTicks) {
if (_map.TryGetValue(key, out tuple)) {
if (_expireTicks == -1) return tuple.Item2;
if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
}
lastTicks = _lastTicks;
}
} finally { _lock.ExitReadLock(); }
_slimLock.EnterWriteLock();
try {
if (_lockDict.TryGetValue(key, out slim) == false) {
slim = new AntiDupLockSlim();
_lockDict[key] = slim;
}
slim.UseCount++;
} finally { _slimLock.ExitWriteLock(); }
} finally { _slimLock.ExitUpgradeableReadLock(); }
slim.EnterWriteLock();
try {
_lock.EnterReadLock();
try {
if (_lastTicks != lastTicks && _map.TryGetValue(key, out tuple)) {
if (_expireTicks == -1) return tuple.Item2;
if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
}
} finally { _lock.ExitReadLock(); }
var val = factory();
_lock.EnterWriteLock();
try {
_lastTicks = DateTime.Now.Ticks;
_map[key] = Tuple.Create(_lastTicks, val);
if (_maxCount > 0) {
if (_queue.Contains(key) == false) {
_queue.Enqueue(key);
if (_queue.Count > _maxCount) _map.Remove(_queue.Dequeue());
}
}
} finally { _lock.ExitWriteLock(); }
return val;
} finally {
slim.ExitWriteLock();
_slimLock.EnterWriteLock();
try {
slim.UseCount--;
if (slim.UseCount == 0) {
_lockDict.Remove(key);
slim.Dispose();
}
} finally { _slimLock.ExitWriteLock(); }
}
}
/// <summary>
/// 清空
/// </summary>
public void Clear()
{
_lock.EnterWriteLock();
try {
_map.Clear();
_queue.Clear();
_slimLock.EnterWriteLock();
try {
_lockDict.Clear();
} finally {
_slimLock.ExitWriteLock();
}
} finally {
_lock.ExitWriteLock();
}
}
}
代碼分析:
使用兩個(gè)ReaderWriterLockSlim鎖 + 一個(gè)AntiDupLockSlim鎖,實(shí)現(xiàn)并發(fā)功能。
Dictionary<TKey, Tuple<long, TValue>> _map實(shí)現(xiàn)緩存,long類型值記錄時(shí)間,實(shí)現(xiàn)緩存過(guò)期
int _maxCount + Queue<TKey> _queue,_queue 記錄key列隊(duì),當(dāng)數(shù)量大于_maxCount,清除多余緩存。
AntiDupLockSlim繼承ReaderWriterLockSlim,實(shí)現(xiàn)垃圾回收,
代碼使用 :
private readonly static AntiDupCache<int, int> antiDupCache = new AntiDupCache<int, int>(50, 1);
antiDupCache.Execute(key, () => {
....
return val;
});
測(cè)試性能數(shù)據(jù):
----------------------- 開(kāi)始 從1到100 重復(fù)次數(shù):1 單位: ms -----------------------
并發(fā)數(shù)量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并發(fā): 188 93 65 46 38 36 28 31 22 20 18 19
AntiDupCache: 190 97 63 48 37 34 29 30 22 18 17 21
AntiDupQueue: 188 95 63 46 37 33 30 25 21 19 17 21
DictCache: 185 96 64 47 38 33 28 29 22 19 17 21
Cache: 185 186 186 188 188 188 184 179 180 184 184 176
第二次普通并發(fā): 180 92 63 47 38 36 26 28 20 17 16 20
----------------------- 開(kāi)始 從1到100 重復(fù)次數(shù):2 單位: ms -----------------------
并發(fā)數(shù)量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并發(fā): 368 191 124 93 73 61 55 47 44 37 34 44
AntiDupCache: 180 90 66 48 37 31 28 24 21 17 17 22
AntiDupQueue: 181 93 65 46 39 31 27 23 21 19 18 19
DictCache: 176 97 61 46 38 30 31 23 21 18 18 22
Cache: 183 187 186 182 186 185 184 177 181 177 176 177
第二次普通并發(fā): 366 185 127 95 71 62 56 48 43 38 34 43
----------------------- 開(kāi)始 從1到100 重復(fù)次數(shù):4 單位: ms -----------------------
并發(fā)數(shù)量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并發(fā): 726 371 253 190 152 132 106 91 86 74 71 69
AntiDupCache: 189 95 64 49 37 33 28 26 22 19 17 18
AntiDupQueue: 184 97 65 51 39 35 28 24 21 18 17 17
DictCache: 182 95 64 45 39 34 29 23 21 18 18 16
Cache: 170 181 180 184 182 183 181 181 176 179 179 178
第二次普通并發(fā): 723 375 250 186 150 129 107 94 87 74 71 67
----------------------- 開(kāi)始 從1到100 重復(fù)次數(shù):12 單位: ms -----------------------
并發(fā)數(shù)量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并發(fā): 2170 1108 762 569 450 389 325 283 253 228 206 186
AntiDupCache: 182 95 64 51 41 32 28 25 26 20 18 18
AntiDupQueue: 189 93 67 44 37 35 29 30 27 22 20 17
DictCache: 184 97 59 50 38 29 27 26 24 19 18 17
Cache: 174 189 181 184 184 177 182 180 176 176 180 179
第二次普通并發(fā): 2190 1116 753 560 456 377 324 286 249 227 202 189
仿線上環(huán)境,性能測(cè)試數(shù)據(jù):
----------------------- 仿線上環(huán)境 從1到1000 單位: ms -----------------------
并發(fā)數(shù)量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并發(fā): 1852 950 636 480 388 331 280 241 213 198 181 168
AntiDupCache: 1844 949 633 481 382 320 267 239 210 195 174 170
AntiDupQueue: 1835 929 628 479 386 318 272 241 208 194 174 166
DictCache: 1841 935 629 480 378 324 269 241 207 199 176 168
Cache: 1832 1854 1851 1866 1858 1858 1832 1825 1801 1797 1788 1785
第二次普通并發(fā): 1854 943 640 468 389 321 273 237 209 198 177 172
項(xiàng)目:
Github: https://github.com/toolgood/ToolGood.AntiDuplication
Nuget: Install-Package ToolGood.AntiDuplication
后記:
嘗試添加 一個(gè)Queue<AntiDupLockSlim> 或Stack<AntiDupLockSlim> 用來(lái)緩存鎖,后發(fā)現(xiàn)性能效率相差不大,上下浮動(dòng)。
使用 lock關(guān)鍵字加鎖,速度相差不大,代碼看似更簡(jiǎn)單,但隱藏了一個(gè)地雷:一般人使用唯一鍵都是使用string,就意味著可能使用lock(string),鎖定字符串尤其危險(xiǎn),因?yàn)樽址还舱Z(yǔ)言運(yùn)行庫(kù) (CLR)“暫留”。 這意味著整個(gè)程序中任何給定字符串都只有一個(gè)實(shí)例,就是這同一個(gè)對(duì)象表示了所有運(yùn)行的應(yīng)用程序域的所有線程中的該文本。因此,只要在應(yīng)用程序進(jìn)程中的任何位置處具有相同內(nèi)容的字符串上放置了鎖,就將鎖定應(yīng)用程序中該字符串的所有實(shí)例。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Asp.Net中避免重復(fù)提交和彈出提示框的實(shí)例代碼
- ASP.NET中防止頁(yè)面刷新造成表單重復(fù)提交執(zhí)行兩次操作
- asp.net頁(yè)面防止重復(fù)提交示例分享
- asp.net表單提交時(shí)防重復(fù)提交并執(zhí)行前臺(tái)的JS驗(yàn)證
- Asp.net防重復(fù)提交機(jī)制實(shí)現(xiàn)方法
- Asp.Net防止刷新重復(fù)提交數(shù)據(jù)的辦法
- asp.net防止刷新時(shí)重復(fù)提交(可禁用工具條刷新按鈕)
- asp.net 處理F5刷新頁(yè)面重復(fù)提交頁(yè)面的一個(gè)思路
- asp.net 防止用戶通過(guò)后退按鈕重復(fù)提交表單
相關(guān)文章
C#使用checkedListBox1控件鏈接數(shù)據(jù)庫(kù)的方法示例
這篇文章主要介紹了C#使用checkedListBox1控件鏈接數(shù)據(jù)庫(kù)的方法,結(jié)合具體實(shí)例形式分析了數(shù)據(jù)庫(kù)的創(chuàng)建及checkedListBox1控件連接數(shù)據(jù)庫(kù)的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06
C#實(shí)現(xiàn).net頁(yè)面之間傳值傳參方法匯總
這篇文章主要介紹了C#實(shí)現(xiàn).net頁(yè)面之間傳值傳參方法,實(shí)例匯總了幾類常見(jiàn)的傳值傳參的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10
C#簡(jiǎn)單實(shí)現(xiàn)SNMP的方法
這篇文章主要介紹了C#簡(jiǎn)單實(shí)現(xiàn)SNMP的方法,通過(guò)一個(gè)簡(jiǎn)單的自定義類分析了C#實(shí)現(xiàn)SNMP的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
Unity實(shí)現(xiàn)物體運(yùn)動(dòng)軌跡的繪制
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)物體運(yùn)動(dòng)軌跡的繪制,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Unity3D動(dòng)態(tài)生成平面網(wǎng)格
這篇文章主要為大家詳細(xì)介紹了Unity3D動(dòng)態(tài)生成平面網(wǎng)格,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02

