Quick.Data.Redis.pas 18 KB

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