Quick.Data.Redis.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. { ***************************************************************************
  2. Copyright (c) 2015-2021 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 : 15/10/2021
  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. try
  157. Disconnect;
  158. finally
  159. fTCPClient.Free;
  160. end;
  161. except
  162. //avoid closing errors
  163. end;
  164. inherited;
  165. end;
  166. procedure TRedisClient.Disconnect;
  167. begin
  168. if fTCPClient.Connected then
  169. begin
  170. RedisQUIT;
  171. fTCPClient.IOHandler.InputBuffer.Clear;
  172. fTCPClient.IOHandler.WriteBufferFlush;
  173. if fTCPClient.Connected then fTCPClient.Disconnect(False);
  174. end;
  175. fConnected := False;
  176. end;
  177. procedure TRedisClient.Connect;
  178. begin
  179. try
  180. //connect password and database
  181. if not fTCPClient.Connected then
  182. begin
  183. fTCPClient.Host := fHost;
  184. fTCPClient.Port := fPort;
  185. fTCPClient.ConnectTimeout := fConnectionTimeout;
  186. fTCPClient.ReadTimeout := fConnectionTimeout;
  187. fTCPClient.Connect;
  188. if not fTCPClient.Connected then raise ERedisConnectionError.Create('Can''t connect to Redis Server!');
  189. end;
  190. fTCPClient.Socket.Binding.SetKeepAliveValues(True,5000,1000);
  191. if fPassword <> '' then
  192. begin
  193. if not RedisAUTH(fPassword) then raise ERedisAuthError.Create('Redis authentication error!');
  194. end;
  195. if fDataBaseNumber > 0 then
  196. begin
  197. if not RedisSELECT(fDataBaseNumber) then raise ERedisConnectionError.CreateFmt('Can''t select Redis Database "%d"',[fDataBaseNumber]);
  198. end;
  199. fTCPClient.IOHandler.MaxLineLength := MaxInt;
  200. fConnected := True;
  201. except
  202. on E : Exception do raise ERedisConnectionError.CreateFmt('Can''t connect to Redis service %s:%d (%s)',[Self.Host,Self.Port,e.Message]);
  203. end;
  204. end;
  205. function TRedisClient.EscapeString(const json: string): string;
  206. begin
  207. Result := StringReplace(json,'\','\\',[rfReplaceAll]);
  208. Result := StringReplace(Result,'"','\"',[rfReplaceAll]);
  209. Result := StringReplace(Result,#13,'\r',[rfReplaceAll]);
  210. Result := StringReplace(Result,#10,'\n',[rfReplaceAll]);
  211. //Result := StringReplace(Result,'/','\/"',[rfReplaceAll]);
  212. end;
  213. procedure TRedisClient.SetConnectionTimeout(const Value: Integer);
  214. begin
  215. if fConnectionTimeout <> Value then
  216. begin
  217. fConnectionTimeout := Value;
  218. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fConnectionTimeout;
  219. end;
  220. end;
  221. procedure TRedisClient.SetReadTimeout(const Value: Integer);
  222. begin
  223. if fReadTimeout <> Value then
  224. begin
  225. fReadTimeout := Value;
  226. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fReadTimeout;
  227. end;
  228. end;
  229. function TRedisClient.Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse;
  230. begin
  231. Result := Command(aCommand,Format(aArgumentsFormat,aValues));
  232. end;
  233. function TRedisclient.Command(const aCommand : string; const aArguments : string = '') : IRedisResponse;
  234. function TrimResponse(const aResponse : string) : string;
  235. begin
  236. Result := Copy(aResponse,Low(aResponse) + 1, aResponse.Length);
  237. end;
  238. var
  239. res : string;
  240. begin
  241. Result := TRedisResponse.Create;
  242. try
  243. if not fTCPClient.Connected then Connect;
  244. fTCPClient.IOHandler.Write(aCommand + ' ' + aArguments + CRLF);
  245. if fTCPClient.IOHandler.CheckForDataOnSource(fReadTimeout) then
  246. begin
  247. res := fTCPClient.IOHandler.ReadLn;
  248. {$IFDEF DEBUG_REDIS}
  249. TDebugger.Trace(Self,Format('Command "%s"',[res]));
  250. {$ENDIF}
  251. if not res.IsEmpty then
  252. case res[Low(res)] of
  253. '+' :
  254. begin
  255. if res.Contains('+OK') then
  256. begin
  257. Result.IsDone := True;
  258. end
  259. else Result.Response := TrimResponse(res);
  260. end;
  261. '-' : Result.Response := TrimResponse(res);
  262. ':' :
  263. begin
  264. Result.Response := TrimResponse(res);
  265. Result.IsDone := Result.Response.ToInteger > -1;
  266. end;
  267. '$' :
  268. begin
  269. Result.Response := TrimResponse(res);
  270. if IsInteger(Result.Response) then
  271. begin
  272. if Result.Response.ToInteger > -1 then Result.IsDone := True;
  273. end
  274. else Result.IsDone := True;
  275. end;
  276. '*' :
  277. begin
  278. Result.Response := TrimResponse(res);
  279. Result.IsDone := True;
  280. end;
  281. else Result.Response := TrimResponse(res);
  282. end;
  283. end;
  284. except
  285. on E : Exception do raise ERedisCommandError.CreateFmt('%s error: %s',[aCommand,e.message]);
  286. end;
  287. end;
  288. function TRedisClient.RedisRPUSH(const aKey, aValue : string) : Boolean;
  289. begin
  290. Result := Command('RPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  291. end;
  292. function TRedisClient.RedisSELECT(dbIndex: Integer): Boolean;
  293. begin
  294. Result := Command('SELECT',dbIndex.ToString).IsDone;
  295. end;
  296. function TRedisClient.RedisSET(const aKey, aValue: string; aTTLMs: Integer = -1): Boolean;
  297. begin
  298. Result := Command('SET','%s "%s" PX %d',[aKey,EscapeString(aValue),aTTLMs]).IsDone;
  299. end;
  300. function TRedisClient.RedisRPOP(const aKey: string; out oValue: string): Boolean;
  301. begin
  302. Result := False;
  303. if Command('RPOP','%s',[aKey]).IsDone then
  304. begin
  305. oValue := fTCPClient.IOHandler.ReadLn;
  306. Result := True;
  307. end;
  308. end;
  309. function TRedisClient.RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  310. var
  311. response : IRedisResponse;
  312. begin
  313. Result := False;
  314. response := Command('BRPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  315. if response.IsDone then
  316. begin
  317. //if response.Response = '-1' then Exit;
  318. fTCPClient.IOHandler.ReadLn; //$int
  319. fTCPClient.IOHandler.ReadLn; //key
  320. fTCPClient.IOHandler.ReadLn; //$int
  321. oValue := fTCPClient.IOHandler.ReadLn; //value
  322. if not oValue.IsEmpty then Result := True;
  323. end
  324. else
  325. begin
  326. if not response.Response.IsEmpty then ERedisCommandError.CreateFmt('BRPOP Error: %s',[response.Response]);
  327. end;
  328. end;
  329. function TRedisClient.RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs: Integer): Boolean;
  330. var
  331. response : IRedisResponse;
  332. begin
  333. Result := False;
  334. response := Command('BRPOPLPUSH','%s %s %d',[aKey,aKeyToMove,aWaitTimeoutSecs]);
  335. if response.IsDone then
  336. begin
  337. oValue := fTCPClient.IOHandler.ReadLn; //value
  338. if not oValue.IsEmpty then Result := True;
  339. end
  340. else raise ERedisCommandError.CreateFmt('BRPOPLPUSH Error: %s',[response.Response]);
  341. end;
  342. function TRedisClient.RedisDEL(const aKey: string): Boolean;
  343. begin
  344. Result := Command('DEL',aKey).IsDone;
  345. end;
  346. function TRedisClient.RedisLLEN(const aKey : string): Integer;
  347. var
  348. response : IRedisResponse;
  349. begin
  350. Result := 0;
  351. response := Command('LLEN',aKey);
  352. if response.IsDone then
  353. begin
  354. Result := response.Response.ToInteger;
  355. end;
  356. end;
  357. function TRedisClient.RedisTTL(const aKey, aValue : string): Integer;
  358. var
  359. response : IRedisResponse;
  360. begin
  361. Result := 0;
  362. response := Command('TTL','%s "%s"',[aKey,EscapeString(aValue)]);
  363. if response.IsDone then
  364. begin
  365. Result := response.Response.ToInteger;
  366. end;
  367. end;
  368. function TRedisClient.RedisZADD(const aKey, aValue: string; aScore: Int64): Boolean;
  369. var
  370. response : IRedisResponse;
  371. begin
  372. response := Command('ZADD','%s %d "%s"',[aKey,aScore,EscapeString(aValue)]);
  373. if response.IsDone then
  374. begin
  375. Result := response.Response.ToInteger = 1;
  376. end
  377. else raise ERedisCommandError.CreateFmt('ZADD %s',[response.Response]);
  378. end;
  379. function TRedisClient.RedisZRANGE(const aKey: string; aStartPosition, aEndPosition: Int64): TArray<string>;
  380. var
  381. response : IRedisResponse;
  382. value : string;
  383. i : Integer;
  384. begin
  385. Result := [];
  386. response := Command('ZRANGE','%s %d %d',[aKey,aStartPosition,aEndPosition]);
  387. if response.IsDone then
  388. begin
  389. for i := 1 to (response.Response.ToInteger) do
  390. begin
  391. fTCPClient.IOHandler.ReadLn; //$int
  392. value := fTCPClient.IOHandler.ReadLn; //value
  393. Result := Result + [value];
  394. end;
  395. end
  396. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  397. end;
  398. function TRedisClient.RedisZRANGEBYSCORE(const aKey: string; aMinScore, aMaxScore: Int64): TArray<TRedisSortedItem>;
  399. var
  400. response : IRedisResponse;
  401. item : TRedisSortedItem;
  402. i : Integer;
  403. value : string;
  404. score : string;
  405. begin
  406. Result := [];
  407. response := Command('ZRANGEBYSCORE','%s %d %d WITHSCORES',[aKey,aMinScore,aMaxScore]);
  408. if response.IsDone then
  409. begin
  410. for i := 1 to (response.Response.ToInteger Div 2) do
  411. begin
  412. fTCPClient.IOHandler.ReadLn; //$int
  413. value := fTCPClient.IOHandler.ReadLn; //value
  414. fTCPClient.IOHandler.ReadLn; //$int
  415. score := fTCPClient.IOHandler.ReadLn; //score
  416. item.Value := value;
  417. item.Score := score.ToInt64;
  418. Result := Result + [item];
  419. end;
  420. end
  421. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  422. end;
  423. function TRedisClient.RedisZREM(const aKey, aValue: string): Boolean;
  424. var
  425. response : IRedisResponse;
  426. begin
  427. Result := False;
  428. response := Command('ZREM','%s "%s"',[aKey,EscapeString(aValue)]);
  429. if response.IsDone then
  430. begin
  431. Result := response.Response.ToInteger = 1;
  432. end;
  433. end;
  434. function TRedisClient.RedisLPOP(const aKey: string; out oValue: string): Boolean;
  435. begin
  436. Result := False;
  437. if Command('LPOP','%s',[aKey]).IsDone then
  438. begin
  439. oValue := fTCPClient.IOHandler.ReadLn;
  440. Result := True;
  441. end;
  442. end;
  443. function TRedisClient.RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  444. var
  445. response : IRedisResponse;
  446. begin
  447. response := Command('BLPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  448. if response.IsDone then
  449. begin
  450. fTCPClient.IOHandler.ReadLn; //$int
  451. fTCPClient.IOHandler.ReadLn; //key
  452. fTCPClient.IOHandler.ReadLn; //$int
  453. oValue := fTCPClient.IOHandler.ReadLn; //value
  454. Result := True;
  455. end
  456. else raise ERedisCommandError.CreateFmt('BLPOP Error: %s',[response.Response]);
  457. end;
  458. function TRedisClient.RedisLPUSH(const aKey, aValue : string) : Boolean;
  459. begin
  460. Result := Command('LPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  461. end;
  462. function TRedisClient.RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  463. begin
  464. Result := Command('LREM','%s %d "%s"',[aKey,aNumOccurrences * -1,EscapeString(aValue)]).IsDone;
  465. end;
  466. function TRedisClient.RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  467. begin
  468. Result := Command('LTRIM','%s %d %d',[aKey,aFirstElement,fMaxSize]).IsDone;
  469. end;
  470. function TRedisClient.RedisAUTH(const aPassword : string) : Boolean;
  471. begin
  472. Result := Command('AUTH',fPassword).IsDone;
  473. end;
  474. function TRedisClient.RedisEXPIRE(const aKey: string; aExpireDate: TDateTime): Boolean;
  475. begin
  476. Result := RedisEXPIRE(aKey,MilliSecondsBetween(Now(),aExpireDate));
  477. end;
  478. function TRedisClient.RedisEXPIRE(const aKey: string; aTTLMs: Integer): Boolean;
  479. begin
  480. Result := Command('PEXPIRE','%s %d',[aKey,aTTLMs]).IsDone;
  481. end;
  482. function TRedisClient.RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  483. var
  484. response : IRedisResponse;
  485. begin
  486. Result := False;
  487. response := Command('LINDEX','%s %d',[aKey,aIndex]);
  488. if response.IsDone then
  489. begin
  490. oValue := response.response;
  491. Result := True;
  492. end;
  493. end;
  494. function TRedisClient.RedisGET(const aKey: string; out oValue: string): Boolean;
  495. begin
  496. Result := False;
  497. if Command('GET','%s',[aKey]).IsDone then
  498. begin
  499. oValue := fTCPClient.IOHandler.ReadLn;
  500. Result := True;
  501. end;
  502. end;
  503. function TRedisClient.RedisPING : Boolean;
  504. begin
  505. Result := False;
  506. if Command('PING').IsDone then
  507. begin
  508. Result := fTCPClient.IOHandler.ReadLn = 'PONG';
  509. end;
  510. end;
  511. function TRedisClient.RedisQUIT : Boolean;
  512. begin
  513. try
  514. Result := Command('QUIT').IsDone;
  515. except
  516. Result := False;
  517. end;
  518. end;
  519. end.