好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Spring Boot实现数据访问计数器方案详解

1、数据访问计数器

  在Spring Boot项目中,有时需要数据访问计数器。大致有下列三种情形:

1)纯计数:如登录的密码错误计数,超过门限N次,则表示计数器满,此时可进行下一步处理,如锁定该账户。

2)时间滑动窗口:设窗口宽度为T,如果窗口中尾帧时间与首帧时间差大于T,则表示计数器满。

  例如使用redis缓存时,使用key查询redis中数据,如果有此key数据,则返回对象数据;如无此key数据,则查询数据库,但如果一直都无此key数据,从而反复查询数据库,显然有问题。此时,可使用时间滑动窗口,对于查询的失败的key,距离首帧T时间(如1分钟)内,不再查询数据库,而是直接返回无此数据,直到新查询的时间超过T,更新滑窗首帧为新时间,并执行一次查询数据库操作。

3)时间滑动窗口+计数:这往往在需要进行限流处理的场景使用。如T时间(如1分钟)内,相同key的访问次数超过超过门限N,则表示计数器满,此时进行限流处理。

2、代码实现

2.1、方案说明

1)使用字典来管理不同的key,因为不同的key需要单独计数。

2)上述三种情况,使用类型属性区分,并在构造函数中进行设置。

3)滑动窗口使用双向队列Deque来实现。

4)考虑到访问并发性,读取或更新时,加锁保护。

2.2、代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

package com.abc.example.service;

 

import java.util.ArrayDeque;

import java.util.Deque;

import java.util.HashMap;

import java.util. Map ;

 

 

/ * *

  * @className   : DacService

  * @description : 数据访问计数服务类

  * @summary     :

  * @history     :

  * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

  * date         version     modifier        remarks                  

  * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

  * 2021 / 08 / 03    1.0 . 0        sheng.zheng     初版

  *

  * /

public class DacService {

    

     / / 计数器类型: 1 - 数量; 2 - 时间窗口; 3 - 时间窗口 + 数量

     private int counterType;

    

     / / 计数器数量门限

     private int counterThreshold = 5 ;

    

     / / 时间窗口长度,单位毫秒

     private int windowSize = 60000 ;

    

     / / 对象key的访问计数器

     private Map <String,Integer> itemMap;

 

     / / 对象key的访问滑动窗口

     private Map <String,Deque< Long >> itemSlideWindowMap;

    

     / * *

      * 构造函数

      * @param counterType       : 计数器类型,值为 1 , 2 , 3 之一

      * @param counterThreshold  : 计数器数量门限,如果类型为 1 或 3 ,需要此值

      * @param windowSize        : 窗口时间长度,如果为类型为 2 , 3 ,需要此值

      * /

     public DacService( int counterType, int counterThreshold, int windowSize) {

         this.counterType = counterType;

         this.counterThreshold = counterThreshold;

         this.windowSize = windowSize;

        

         if (counterType = = 1 ) {

             / / 如果与计数器有关

             itemMap = new HashMap<String,Integer>();

         } else if (counterType = = 2 || counterType = = 3 ) {

             / / 如果与滑动窗口有关

             itemSlideWindowMap = new HashMap<String,Deque< Long >>();

         }

     }      

        

     / * *

      *

      * @methodName      : isItemKeyFull

      * @description     : 对象key的计数是否将满

      * @param itemKey   : 对象key

      * @param timeMillis    : 时间戳,毫秒数,如为滑窗类计数器,使用此参数值

      * @ return       : 满返回true,否则返回false

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 03    1.0 . 0        sheng.zheng     初版

      * 2021 / 08 / 08    1.0 . 1        sheng.zheng     支持多种类型计数器

      *

      * /

     public boolean isItemKeyFull(String itemKey, Long timeMillis) {

         boolean bRet = false;

        

         if (this.counterType = = 1 ) {

             / / 如果为计数器类型        

             if (itemMap.containsKey(itemKey)) {

             synchronized(itemMap) {

                 Integer value = itemMap.get(itemKey);

                 / / 如果计数器将超越门限

                 if (value > = this.counterThreshold - 1 ) {

                     bRet = true;

                 }                  

             }

             } else {

                 / / 新的对象key,视业务需要,取值true或false

             bRet = true;

             }

         } else if (this.counterType = = 2 ){

             / / 如果为滑窗类型         

             if (itemSlideWindowMap.containsKey(itemKey)) {

               Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);

               synchronized(itemQueue) {

                   if (itemQueue.size() > 0 ) {

                   Long head = itemQueue.getFirst();

                   if (timeMillis - head > = this.windowSize) {

                       / / 如果窗口将满

                       bRet = true;

                   }

                   }                                

               }

             } else {

                 / / 新的对象key,视业务需要,取值true或false

             bRet = true;               

             }          

         } else if (this.counterType = = 3 ){

             / / 如果为滑窗 + 数量类型

             if (itemSlideWindowMap.containsKey(itemKey)) {

                 Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);

             synchronized(itemQueue) {

                 Long head = 0L ;

                 / / 循环处理头部数据,确保新数据帧加入后,维持窗口宽度

                 while (true) {

                     / / 取得头部数据

                     head = itemQueue.peekFirst();

                     if (head = = null || timeMillis - head < = this.windowSize) {

                         break ;

                 }

                 / / 移除头部

                 itemQueue.remove();

                 }  

                 if (itemQueue.size() > = this.counterThreshold - 1 ) {

                     / / 如果窗口数量将满

                 bRet = true;

                 }                                          

             }

             } else {

             / / 新的对象key,视业务需要,取值true或false

             bRet = true;               

             }          

         }

        

         return bRet;       

     }

        

     / * *

      *

      * @methodName      : resetItemKey

      * @description     : 复位对象key的计数

      * @param itemKey   : 对象key

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 03    1.0 . 0        sheng.zheng     初版

      * 2021 / 08 / 08    1.0 . 1        sheng.zheng     支持多种类型计数器

      *

      * /

     public void resetItemKey(String itemKey) {

         if (this.counterType = = 1 ) {

             / / 如果为计数器类型

             if (itemMap.containsKey(itemKey)) {

                 / / 更新值,加锁保护

             synchronized(itemMap) {

                 itemMap.put(itemKey, 0 );

             }          

             }      

         } else if (this.counterType = = 2 ){

             / / 如果为滑窗类型

             / / 清空

             if (itemSlideWindowMap.containsKey(itemKey)) {

                 Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);

             if (itemQueue.size() > 0 ) {

                 / / 加锁保护

                 synchronized(itemQueue) {

                   / / 清空

                   itemQueue.clear();

                 }                              

             }

             }                      

         } else if (this.counterType = = 3 ){

             / / 如果为滑窗 + 数量类型

             if (itemSlideWindowMap.containsKey(itemKey)) {

                 Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);

             synchronized(itemQueue) {

                 / / 清空

                 itemQueue.clear();

             }

             }

         }

     }

    

     / * *

      *

      * @methodName      : putItemkey

      * @description     : 更新对象key的计数

      * @param itemKey   : 对象key

      * @param timeMillis    : 时间戳,毫秒数,如为滑窗类计数器,使用此参数值

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 03    1.0 . 0        sheng.zheng     初版

      * 2021 / 08 / 08    1.0 . 1        sheng.zheng     支持多种类型计数器

      *

      * /

     public void putItemkey(String itemKey, Long timeMillis) {

         if (this.counterType = = 1 ) {

             / / 如果为计数器类型

             if (itemMap.containsKey(itemKey)) {

                 / / 更新值,加锁保护

             synchronized(itemMap) {

                 Integer value = itemMap.get(itemKey);

                 / / 计数器 + 1

                 value + + ;

                 itemMap.put(itemKey, value);

             }

             } else {

                 / / 新key值,加锁保护

             synchronized(itemMap) {

                 itemMap.put(itemKey, 1 );

             }          

             }

         } else if (this.counterType = = 2 ){

             / / 如果为滑窗类型 

             if (itemSlideWindowMap.containsKey(itemKey)) {

                 Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);             

             / / 加锁保护

             synchronized(itemQueue) {

                 / / 加入

                 itemQueue.add(timeMillis);

             }                              

             } else {

             / / 新key值,加锁保护

             Deque< Long > itemQueue = new ArrayDeque< Long >();

             synchronized(itemSlideWindowMap) {

                 / / 加入映射表

                 itemSlideWindowMap.put(itemKey, itemQueue);

                 itemQueue.add(timeMillis);

             }

             }

         } else if (this.counterType = = 3 ){

             / / 如果为滑窗 + 数量类型

             if (itemSlideWindowMap.containsKey(itemKey)) {

                 Deque< Long > itemQueue = itemSlideWindowMap.get(itemKey);             

             / / 加锁保护

             synchronized(itemQueue) {

                 Long head = 0L ;

                 / / 循环处理头部数据

                 while (true) {

                     / / 取得头部数据

                 head = itemQueue.peekFirst();

                 if (head = = null || timeMillis - head < = this.windowSize) {

                     break ;

                 }

                 / / 移除头部

                 itemQueue.remove();

                 }

                 / / 加入新数据

                 itemQueue.add(timeMillis);                 

             }                              

             } else {

             / / 新key值,加锁保护

             Deque< Long > itemQueue = new ArrayDeque< Long >();

             synchronized(itemSlideWindowMap) {

                 / / 加入映射表

                 itemSlideWindowMap.put(itemKey, itemQueue);

                 itemQueue.add(timeMillis);

             }

             }          

         }              

     }

        

     / * *

      *

      * @methodName  : clear

      * @description : 清空字典

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 03    1.0 . 0        sheng.zheng     初版

      * 2021 / 08 / 08    1.0 . 1        sheng.zheng     支持多种类型计数器

      *

      * /

     public void clear() {

         if (this.counterType = = 1 ) {

             / / 如果为计数器类型

             synchronized(this) {

                 itemMap.clear();

             }              

         } else if (this.counterType = = 2 ){

             / / 如果为滑窗类型 

             synchronized(this) {

                 itemSlideWindowMap.clear();

             }              

         } else if (this.counterType = = 3 ){

             / / 如果为滑窗 + 数量类型

             synchronized(this) {

                 itemSlideWindowMap.clear();

             }              

         }          

     }

}

2.3、调用

  要调用计数器,只需在应用类中添加DacService对象,如:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

public class DataCommonService {

     / / 数据访问计数服务类,时间滑动窗口,窗口宽度 60 秒

     protected DacService dacService = new DacService( 2 , 0 , 60000 );

 

     / * *

      *

      * @methodName      : procNoClassData

      * @description     : 对象组key对应的数据不存在时的处理

      * @param classKey  : 对象组key

      * @ return       : 数据加载成功,返回true,否则为false

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 08    1.0 . 0        sheng.zheng     初版

      *

      * /

     protected boolean procNoClassData( Object classKey) {

         boolean bRet = false;

         String key = getCombineKey(null,classKey);

         Long currentTime = System.currentTimeMillis();

         / / 判断计数器是否将满

         if (dacService.isItemKeyFull(key,currentTime)) {

             / / 如果计数将满

             / / 复位

             dacService.resetItemKey(key);

             / / 从数据库加载分组数据项

             bRet = loadGroupItems(classKey);

         }

         dacService.putItemkey(key,currentTime);

         return bRet;

     }

    

     / * *

      *

      * @methodName      : procNoItemData

      * @description     : 对象key对应的数据不存在时的处理

      * @param itemKey   : 对象key

      * @param classKey  : 对象组key

      * @ return       : 数据加载成功,返回true,否则为false

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 08    1.0 . 0        sheng.zheng     初版

      *

      * /

     protected boolean procNoItemData( Object itemKey, Object classKey) {

         / / 如果itemKey不存在

         boolean bRet = false;

         String key = getCombineKey(itemKey,classKey);

        

         Long currentTime = System.currentTimeMillis();

         if (dacService.isItemKeyFull(key,currentTime)) {

             / / 如果计数将满

             / / 复位

             dacService.resetItemKey(key);

             / / 从数据库加载数据项

             bRet = loadItem(itemKey, classKey);

         }

         dacService.putItemkey(key,currentTime);        

         return bRet;

     }

 

     / * *

      *

      * @methodName      : getCombineKey

      * @description     : 获取组合key值

      * @param itemKey   : 对象key

      * @param classKey  : 对象组key

      * @ return       : 组合key

      * @history     :

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * date         version     modifier        remarks                  

      * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      * 2021 / 08 / 08    1.0 . 0        sheng.zheng     初版

      *

      * /

     protected String getCombineKey( Object itemKey, Object classKey) {

         String sItemKey = (itemKey = = null ? "" : itemKey.toString());

         String sClassKey = (classKey = = null ? "" : classKey.toString());

         String key = "";

         if (!sClassKey.isEmpty()) {

             key = sClassKey;

         }

         if (!sItemKey.isEmpty()) {

             if (!key.isEmpty()) {

                 key + = "-" + sItemKey;

             } else {

                 key = sItemKey;

             }

         }

         return key;

     }

}

  procNoClassData方法:分组数据不存在时的处理。procNoItemData方法:单个数据项不存在时的处理。

  主从关系在数据库中,较为常见,因此针对分组数据和单个对象key分别编写了方法;如果key的个数超过2个,可以类似处理。

作者:阿拉伯1999 出处:http://www.cnblogs.com/alabo1999/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 养成良好习惯,好文章随手顶一下。

到此这篇关于Spring Boot实现数据访问计数器方案详解的文章就介绍到这了,更多相关Spring Boot数据访问计数器内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

原文链接:https://www.cnblogs.com/alabo1999/p/15115695.html

查看更多关于Spring Boot实现数据访问计数器方案详解的详细内容...

  阅读:20次