2
0

Quick.Data.Redis.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. { ***************************************************************************
  2. Copyright (c) 2015-2020 Kike Pérez
  3. Unit : Quick.Data.Redis
  4. Description : Redis client
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 22/02/2020
  8. Modified : 12/07/2020
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.Data.Redis;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. {$IFDEF DEBUG_REDIS}
  26. Quick.Debug.Utils,
  27. {$ENDIF}
  28. System.SysUtils,
  29. System.DateUtils,
  30. IdTCPClient,
  31. Quick.Commons;
  32. type
  33. IRedisResponse = interface
  34. ['{21EF7ABF-E678-4F18-AE56-8A7C6B817AE3}']
  35. function GetIsDone: Boolean;
  36. function GetResponse: string;
  37. procedure SetIsDone(const Value: Boolean);
  38. procedure SetResponse(const Value: string);
  39. property IsDone : Boolean read GetIsDone write SetIsDone;
  40. property Response : string read GetResponse write SetResponse;
  41. end;
  42. TRedisResponse = class(TInterfacedObject,IRedisResponse)
  43. private
  44. fIsDone : Boolean;
  45. fResponse : string;
  46. function GetIsDone: Boolean;
  47. function GetResponse: string;
  48. procedure SetIsDone(const Value: Boolean);
  49. procedure SetResponse(const Value: string);
  50. public
  51. property IsDone : Boolean read GetIsDone write SetIsDone;
  52. property Response : string read GetResponse write SetResponse;
  53. end;
  54. TRedisSortedItem = record
  55. Value : string;
  56. Score : Int64;
  57. end;
  58. TRedisClient = class
  59. private
  60. fTCPClient : TIdTCPClient;
  61. fHost : string;
  62. fPort : Integer;
  63. fDataBaseNumber : Integer;
  64. fMaxSize : Int64;
  65. fPassword : string;
  66. fConnectionTimeout : Integer;
  67. fReadTimeout : Integer;
  68. fConnected : Boolean;
  69. procedure SetConnectionTimeout(const Value: Integer);
  70. procedure SetReadTimeout(const Value: Integer);
  71. function Command(const aCommand : string; const aArguments : string = '') : IRedisResponse; overload;
  72. function Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse; overload;
  73. function EscapeString(const json: string) : string;
  74. public
  75. constructor Create;
  76. destructor Destroy; override;
  77. property Host : string read fHost write fHost;
  78. property Port : Integer read fPort write fPort;
  79. property DataBaseNumber : Integer read fDataBaseNumber write fDataBaseNumber;
  80. property MaxSize : Int64 read fMaxSize write fMaxSize;
  81. property Password : string read fPassword write fPassword;
  82. property ConnectionTimeout : Integer read fConnectionTimeout write SetConnectionTimeout;
  83. property ReadTimeout : Integer read fReadTimeout write SetReadTimeout;
  84. property Connected : Boolean read fConnected;
  85. function RedisSELECT(dbIndex : Integer) : Boolean;
  86. function RedisSET(const aKey, aValue : string; aTTLMs : Integer = -1) : Boolean;
  87. function RedisGET(const aKey : string; out oValue : string) : Boolean;
  88. function RedisDEL(const aKey : string) : Boolean;
  89. function RedisRPUSH(const aKey, aValue : string) : Boolean;
  90. function RedisLPUSH(const aKey, aValue : string) : Boolean;
  91. function RedisRPOP(const aKey : string; out oValue : string) : Boolean;
  92. function RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  93. function RedisLPOP(const aKey : string; out oValue : string) : Boolean;
  94. function RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  95. function RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  96. function RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  97. function RedisEXPIRE(const aKey : string; aTTLMs : Integer) : Boolean; overload;
  98. function RedisEXPIRE(const aKey : string; aExpireDate : TDateTime) : Boolean; overload;
  99. function RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  100. function RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  101. function RedisZADD(const aKey, aValue : string; aScore : Int64) : Boolean;
  102. function RedisZREM(const aKey, aValue : string) : Boolean;
  103. function RedisZRANGE(const aKey : string; aStartPosition, aEndPosition : Int64) : TArray<string>;
  104. function RedisZRANGEBYSCORE(const aKey : string; aMinScore, aMaxScore : Int64) : TArray<TRedisSortedItem>;
  105. function RedisLLEN(const aKey : string): Integer;
  106. function RedisTTL(const aKey, aValue : string): Integer;
  107. function RedisAUTH(const aPassword : string) : Boolean;
  108. function RedisPING : Boolean;
  109. function RedisQUIT : Boolean;
  110. procedure Connect;
  111. procedure Disconnect;
  112. end;
  113. ERedisConnectionError = class(Exception);
  114. ERedisAuthError = class(Exception);
  115. ERedisCommandError = class(Exception);
  116. implementation
  117. const
  118. DEF_REDIS_PORT = 6379;
  119. CRLF = #10#13;
  120. DEF_CONNECTIONTIMEOUT = 30000;
  121. DEF_READTIMETOUT = 10000;
  122. { TRedisResponse }
  123. function TRedisResponse.GetIsDone: Boolean;
  124. begin
  125. Result := fIsDone;
  126. end;
  127. function TRedisResponse.GetResponse: string;
  128. begin
  129. Result := fResponse;
  130. end;
  131. procedure TRedisResponse.SetIsDone(const Value: Boolean);
  132. begin
  133. fIsDone := Value;
  134. end;
  135. procedure TRedisResponse.SetResponse(const Value: string);
  136. begin
  137. fResponse := Value;
  138. end;
  139. { TRedisClient }
  140. constructor TRedisClient.Create;
  141. begin
  142. inherited;
  143. fConnected := False;
  144. fHost := 'localhost';
  145. fPort := DEF_REDIS_PORT;
  146. fDataBaseNumber := 0;
  147. fMaxSize := 0;
  148. fPassword := '';
  149. fConnectionTimeout := DEF_CONNECTIONTIMEOUT;
  150. fReadTimeout := DEF_READTIMETOUT;
  151. fTCPClient := TIdTCPClient.Create;
  152. end;
  153. destructor TRedisClient.Destroy;
  154. begin
  155. try
  156. if fTCPClient.Connected then RedisQUIT;
  157. fTCPClient.IOHandler.InputBuffer.Clear;
  158. fTCPClient.IOHandler.WriteBufferFlush;
  159. if fTCPClient.Connected then fTCPClient.Disconnect(False);
  160. fTCPClient.Free;
  161. except
  162. //avoid closing errors
  163. end;
  164. inherited;
  165. end;
  166. procedure TRedisClient.Disconnect;
  167. begin
  168. if fConnected then RedisQUIT;
  169. fConnected := False;
  170. end;
  171. procedure TRedisClient.Connect;
  172. begin
  173. if not fTCPClient.Connected then
  174. begin
  175. fTCPClient.Host := fHost;
  176. fTCPClient.Port := fPort;
  177. fTCPClient.ConnectTimeout := fConnectionTimeout;
  178. fTCPClient.ReadTimeout := fConnectionTimeout;
  179. end;
  180. try
  181. fTCPClient.Connect; //first connection
  182. //connect password and database
  183. if not fTCPClient.Connected then
  184. begin
  185. fTCPClient.Connect;
  186. if not fTCPClient.Connected then raise ERedisConnectionError.Create('Can''t connect to Redis Server!');
  187. end;
  188. fTCPClient.Socket.Binding.SetKeepAliveValues(True,5000,1000);
  189. if fPassword <> '' then
  190. begin
  191. if not RedisAUTH(fPassword) then raise ERedisAuthError.Create('Redis authentication error!');
  192. end;
  193. if fDataBaseNumber > 0 then
  194. begin
  195. if not RedisSELECT(fDataBaseNumber) then raise ERedisConnectionError.CreateFmt('Can''t select Redis Database "%d"',[fDataBaseNumber]);
  196. end;
  197. fConnected := True;
  198. except
  199. on E : Exception do raise ERedisConnectionError.CreateFmt('Can''t connect to Redis service %s:%d (%s)',[Self.Host,Self.Port,e.Message]);
  200. end;
  201. end;
  202. function TRedisClient.EscapeString(const json: string): string;
  203. begin
  204. Result := StringReplace(json,'\','\\',[rfReplaceAll]);
  205. Result := StringReplace(Result,'"','\"',[rfReplaceAll]);
  206. //Result := StringReplace(Result,'/','\/"',[rfReplaceAll]);
  207. end;
  208. procedure TRedisClient.SetConnectionTimeout(const Value: Integer);
  209. begin
  210. if fConnectionTimeout <> Value then
  211. begin
  212. fConnectionTimeout := Value;
  213. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fConnectionTimeout;
  214. end;
  215. end;
  216. procedure TRedisClient.SetReadTimeout(const Value: Integer);
  217. begin
  218. if fReadTimeout <> Value then
  219. begin
  220. fReadTimeout := Value;
  221. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fReadTimeout;
  222. end;
  223. end;
  224. function TRedisClient.Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse;
  225. begin
  226. Result := Command(aCommand,Format(aArgumentsFormat,aValues));
  227. end;
  228. function TRedisclient.Command(const aCommand : string; const aArguments : string = '') : IRedisResponse;
  229. function TrimResponse(const aResponse : string) : string;
  230. begin
  231. Result := Copy(aResponse,Low(aResponse) + 1, aResponse.Length);
  232. end;
  233. var
  234. res : string;
  235. begin
  236. Result := TRedisResponse.Create;
  237. try
  238. if not fTCPClient.Connected then Connect;
  239. fTCPClient.IOHandler.Write(aCommand + ' ' + aArguments + CRLF);
  240. if fTCPClient.IOHandler.CheckForDataOnSource(fReadTimeout) then
  241. begin
  242. res := fTCPClient.IOHandler.ReadLn;
  243. {$IFDEF DEBUG_REDIS}
  244. TDebugger.Trace(Self,Format('Command "%s"',[res]));
  245. {$ENDIF}
  246. if not res.IsEmpty then
  247. case res[Low(res)] of
  248. '+' :
  249. begin
  250. if res.Contains('+OK') then
  251. begin
  252. Result.IsDone := True;
  253. end
  254. else Result.Response := TrimResponse(res);
  255. end;
  256. '-' : Result.Response := TrimResponse(res);
  257. ':' :
  258. begin
  259. Result.Response := TrimResponse(res);
  260. Result.IsDone := Result.Response.ToInteger > -1;
  261. end;
  262. '$' :
  263. begin
  264. Result.Response := TrimResponse(res);
  265. if IsInteger(Result.Response) then
  266. begin
  267. if Result.Response.ToInteger > -1 then Result.IsDone := True;
  268. end
  269. else Result.IsDone := True;
  270. end;
  271. '*' :
  272. begin
  273. Result.Response := TrimResponse(res);
  274. Result.IsDone := True;
  275. end;
  276. end;
  277. end;
  278. except
  279. on E : Exception do raise ERedisCommandError.CreateFmt('%s error: %s',[aCommand,e.message]);
  280. end;
  281. end;
  282. function TRedisClient.RedisRPUSH(const aKey, aValue : string) : Boolean;
  283. begin
  284. Result := Command('RPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  285. end;
  286. function TRedisClient.RedisSELECT(dbIndex: Integer): Boolean;
  287. begin
  288. Result := Command('SELECT',dbIndex.ToString).IsDone;
  289. end;
  290. function TRedisClient.RedisSET(const aKey, aValue: string; aTTLMs: Integer = -1): Boolean;
  291. begin
  292. Result := Command('SET','%s "%s" PX %d',[aKey,EscapeString(aValue),aTTLMs]).IsDone;
  293. end;
  294. function TRedisClient.RedisRPOP(const aKey: string; out oValue: string): Boolean;
  295. begin
  296. Result := False;
  297. if Command('RPOP','%s',[aKey]).IsDone then
  298. begin
  299. oValue := fTCPClient.IOHandler.ReadLn;
  300. Result := True;
  301. end;
  302. end;
  303. function TRedisClient.RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  304. var
  305. response : IRedisResponse;
  306. begin
  307. Result := False;
  308. response := Command('BRPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  309. if response.IsDone then
  310. begin
  311. //if response.Response = '-1' then Exit;
  312. fTCPClient.IOHandler.ReadLn; //$int
  313. fTCPClient.IOHandler.ReadLn; //key
  314. fTCPClient.IOHandler.ReadLn; //$int
  315. oValue := fTCPClient.IOHandler.ReadLn; //value
  316. Result := True;
  317. end
  318. else
  319. begin
  320. if not response.Response.IsEmpty then ERedisCommandError.CreateFmt('BRPOP Error: %s',[response.Response]);
  321. end;
  322. end;
  323. function TRedisClient.RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs: Integer): Boolean;
  324. var
  325. response : IRedisResponse;
  326. begin
  327. response := Command('BRPOPLPUSH','%s %s %d',[aKey,aKeyToMove,aWaitTimeoutSecs]);
  328. if response.IsDone then
  329. begin
  330. oValue := fTCPClient.IOHandler.ReadLn; //value
  331. Result := True;
  332. end
  333. else raise ERedisCommandError.CreateFmt('BRPOPLPUSH Error: %s',[response.Response]);
  334. end;
  335. function TRedisClient.RedisDEL(const aKey: string): Boolean;
  336. begin
  337. Result := Command('DEL',aKey).IsDone;
  338. end;
  339. function TRedisClient.RedisLLEN(const aKey : string): Integer;
  340. var
  341. response : IRedisResponse;
  342. begin
  343. Result := 0;
  344. response := Command('LLEN',aKey);
  345. if response.IsDone then
  346. begin
  347. Result := response.Response.ToInteger;
  348. end;
  349. end;
  350. function TRedisClient.RedisTTL(const aKey, aValue : string): Integer;
  351. var
  352. response : IRedisResponse;
  353. begin
  354. Result := 0;
  355. response := Command('TTL','%s "%s"',[aKey,EscapeString(aValue)]);
  356. if response.IsDone then
  357. begin
  358. Result := response.Response.ToInteger;
  359. end;
  360. end;
  361. function TRedisClient.RedisZADD(const aKey, aValue: string; aScore: Int64): Boolean;
  362. var
  363. response : IRedisResponse;
  364. begin
  365. response := Command('ZADD','%s %d "%s"',[aKey,aScore,EscapeString(aValue)]);
  366. if response.IsDone then
  367. begin
  368. Result := response.Response.ToInteger = 1;
  369. end
  370. else raise ERedisCommandError.CreateFmt('ZADD %s',[response.Response]);
  371. end;
  372. function TRedisClient.RedisZRANGE(const aKey: string; aStartPosition, aEndPosition: Int64): TArray<string>;
  373. var
  374. response : IRedisResponse;
  375. value : string;
  376. i : Integer;
  377. begin
  378. Result := [];
  379. response := Command('ZRANGE','%s %d %d',[aKey,aStartPosition,aEndPosition]);
  380. if response.IsDone then
  381. begin
  382. for i := 1 to (response.Response.ToInteger) do
  383. begin
  384. fTCPClient.IOHandler.ReadLn; //$int
  385. value := fTCPClient.IOHandler.ReadLn; //value
  386. Result := Result + [value];
  387. end;
  388. end
  389. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  390. end;
  391. function TRedisClient.RedisZRANGEBYSCORE(const aKey: string; aMinScore, aMaxScore: Int64): TArray<TRedisSortedItem>;
  392. var
  393. response : IRedisResponse;
  394. item : TRedisSortedItem;
  395. i : Integer;
  396. value : string;
  397. score : string;
  398. begin
  399. Result := [];
  400. response := Command('ZRANGEBYSCORE','%s %d %d WITHSCORES',[aKey,aMinScore,aMaxScore]);
  401. if response.IsDone then
  402. begin
  403. for i := 1 to (response.Response.ToInteger Div 2) do
  404. begin
  405. fTCPClient.IOHandler.ReadLn; //$int
  406. value := fTCPClient.IOHandler.ReadLn; //value
  407. fTCPClient.IOHandler.ReadLn; //$int
  408. score := fTCPClient.IOHandler.ReadLn; //score
  409. item.Value := value;
  410. item.Score := score.ToInt64;
  411. Result := Result + [item];
  412. end;
  413. end
  414. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  415. end;
  416. function TRedisClient.RedisZREM(const aKey, aValue: string): Boolean;
  417. var
  418. response : IRedisResponse;
  419. begin
  420. Result := False;
  421. response := Command('ZREM','%s "%s"',[aKey,EscapeString(aValue)]);
  422. if response.IsDone then
  423. begin
  424. Result := response.Response.ToInteger = 1;
  425. end;
  426. end;
  427. function TRedisClient.RedisLPOP(const aKey: string; out oValue: string): Boolean;
  428. begin
  429. Result := False;
  430. if Command('LPOP','%s',[aKey]).IsDone then
  431. begin
  432. oValue := fTCPClient.IOHandler.ReadLn;
  433. Result := True;
  434. end;
  435. end;
  436. function TRedisClient.RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  437. var
  438. response : IRedisResponse;
  439. begin
  440. response := Command('BLPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  441. if response.IsDone then
  442. begin
  443. fTCPClient.IOHandler.ReadLn; //$int
  444. fTCPClient.IOHandler.ReadLn; //key
  445. fTCPClient.IOHandler.ReadLn; //$int
  446. oValue := fTCPClient.IOHandler.ReadLn; //value
  447. Result := True;
  448. end
  449. else raise ERedisCommandError.CreateFmt('BLPOP Error: %s',[response.Response]);
  450. end;
  451. function TRedisClient.RedisLPUSH(const aKey, aValue : string) : Boolean;
  452. begin
  453. Result := Command('LPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  454. end;
  455. function TRedisClient.RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  456. begin
  457. Result := Command('LREM','%s "%s" %d',[aKey,EscapeString(aValue),aNumOccurrences * -1]).IsDone;
  458. end;
  459. function TRedisClient.RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  460. begin
  461. Result := Command('LTRIM','%s %d %d',[aKey,aFirstElement,fMaxSize]).IsDone;
  462. end;
  463. function TRedisClient.RedisAUTH(const aPassword : string) : Boolean;
  464. begin
  465. Result := Command('AUTH',fPassword).IsDone;
  466. end;
  467. function TRedisClient.RedisEXPIRE(const aKey: string; aExpireDate: TDateTime): Boolean;
  468. begin
  469. Result := RedisEXPIRE(aKey,MilliSecondsBetween(Now(),aExpireDate));
  470. end;
  471. function TRedisClient.RedisEXPIRE(const aKey: string; aTTLMs: Integer): Boolean;
  472. begin
  473. Result := Command('PEXPIRE','%s %d',[aKey,aTTLMs]).IsDone;
  474. end;
  475. function TRedisClient.RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  476. var
  477. response : IRedisResponse;
  478. begin
  479. Result := False;
  480. response := Command('LINDEX','%s %d',[aKey,aIndex]);
  481. if response.IsDone then
  482. begin
  483. oValue := response.response;
  484. Result := True;
  485. end;
  486. end;
  487. function TRedisClient.RedisGET(const aKey: string; out oValue: string): Boolean;
  488. begin
  489. Result := False;
  490. if Command('GET','%s',[aKey]).IsDone then
  491. begin
  492. oValue := fTCPClient.IOHandler.ReadLn;
  493. Result := True;
  494. end;
  495. end;
  496. function TRedisClient.RedisPING : Boolean;
  497. begin
  498. Result := False;
  499. if Command('PING').IsDone then
  500. begin
  501. Result := fTCPClient.IOHandler.ReadLn = 'PONG';
  502. end;
  503. end;
  504. function TRedisClient.RedisQUIT : Boolean;
  505. begin
  506. try
  507. Result := Command('QUIT').IsDone;
  508. except
  509. Result := False;
  510. end;
  511. end;
  512. end.